@StepScope 与 @JobScope
本章定位:深入理解 Spring Batch 的两个自定义作用域,掌握延迟绑定、参数注入和状态上下文访问的完整用法。
定义与作用
@StepScope 和 @JobScope 是 Spring Batch 自定义的 Bean 作用域。它们使 Bean 的创建延迟到 Step/Job 启动时,从而能够注入运行时的 JobParameters 和 ExecutionContext 值。
@Bean
@StepScope
public FlatFileItemReader<Order> reader(
@Value("#{jobParameters['input.file']}") String inputFile) {
// inputFile 在 Step 启动时才可用
return new FlatFileItemReaderBuilder<Order>()
.resource(new FileSystemResource(inputFile))
.build();
}
小崔的感悟:
"没 @StepScope 之前,你得在代码里手动获取 JobParameters——又丑又绕。有了它,直接
@Value注入,像普通 Spring Bean 一样用,框架帮你延迟到正确的时机创建。"
核心原理
两个作用域对比
| 维度 | @StepScope | @JobScope |
|---|---|---|
| 生命周期 | 单个 Step 执行期间 | 单个 Job 执行期间 |
| 创建时机 | Step 启动时 | Job 启动时 |
| 可注入 | jobParameters、stepExecutionContext | jobParameters、jobExecutionContext |
| 适用 Bean | ItemReader、ItemProcessor、ItemWriter | Job 级别的 Listener、Decider |
| 典型场景 | 动态文件路径、分区上下文 | 作业级别配置 |
完整示例
场景一:@StepScope 绑定动态文件路径
@Configuration
public class StepScopeConfig {
@Bean
@StepScope
public FlatFileItemReader<Order> orderReader(
@Value("#{jobParameters['input.directory']}") String dir,
@Value("#{jobParameters['input.date']}") String date) {
// 用 JobParameters 动态拼接文件路径
String filePath = dir + "/orders_" + date + ".csv";
return new FlatFileItemReaderBuilder<Order>()
.name("orderReader")
.resource(new FileSystemResource(filePath))
.delimited()
.names("orderId", "customerName", "amount")
.targetType(Order.class)
.build();
}
}
场景二:@StepScope 读取分区上下文
@Bean
@StepScope
public JdbcPagingItemReader<Order> partitionReader(
DataSource dataSource,
@Value("#{stepExecutionContext['minId']}") int minId,
@Value("#{stepExecutionContext['maxId']}") int maxId) {
// 每个 Worker 获取自己的分区范围
return new JdbcPagingItemReaderBuilder<Order>()
.name("partitionReader_" + minId + "_" + maxId)
.dataSource(dataSource)
.selectClause("SELECT *")
.fromClause("FROM orders")
.whereClause("WHERE id BETWEEN :minId AND :maxId")
.parameterValues(Map.of("minId", minId, "maxId", maxId))
.sortKeys(Map.of("id", Order.ASCENDING))
.pageSize(500)
.rowMapper(new BeanPropertyRowMapper<>(Order.class))
.build();
}
场景三:@JobScope 注入到 Listener
@Component
@JobScope
public class JobReportListener implements JobExecutionListener {
@Value("#{jobParameters['report.email']}")
private String reportEmail;
@Value("#{jobParameters['report.type']}")
private String reportType;
@Override
public void afterJob(JobExecution jobExecution) {
if (jobExecution.getStatus() == BatchStatus.COMPLETED) {
System.out.println("发送 " + reportType + " 报表到 " + reportEmail);
// emailService.send(reportEmail, generateReport(reportType));
}
}
}
易错场景与避坑
反例一:无 @StepScope 时注入 JobParameters
// ❌ 没有 @StepScope → Bean 在容器启动时创建
@Bean
public FlatFileItemReader<Order> reader(
@Value("#{jobParameters['input.file']}") String file) {
// JobParameters 此时还不存在 → SpEL 解析失败
return new FlatFileItemReaderBuilder<Order>()
.resource(new FileSystemResource(file))
.build();
}
// 容器启动报错: EvaluationException
反例二:@StepScope Bean 被单例 Bean 注入
// ❌ 单例 Service 注入 @StepScope Reader
@Service
public class BatchService {
@Autowired
private FlatFileItemReader<Order> reader; // ❌ 单例持有 StepScope Bean
// 需要用 @Scope(proxyMode=ScopedProxyMode.TARGET_CLASS)
}
正确做法:@StepScope 默认使用 TARGET_CLASS 的 CGLIB 代理,Spring Boot 会自动处理。如果在纯 Spring 环境需要手动配置 ScopedProxyMode。
面试高频考点
Q1:@StepScope 的底层实现原理?
基于 Spring 的自定义作用域机制(
Scope接口)。StepScope将 Bean 实例存储在 Step 的stepContext属性中,Step 启动时创建、Step 结束时销毁。SpEL 表达式#{jobParameters['xxx']}在 Bean 创建时解析。
Q2:为什么 Reader/Writer 默认应该是 @StepScope?
①可以注入
JobParameters获取动态配置;②分区场景下每个 Worker 需要不同的数据范围,通过#{stepExecutionContext['xxx']}注入;③避免单例 Bean 在多次 Job 执行间共享状态导致线程安全问题。
上一章:ItemStream 与状态管理下一章:ItemReader 详解