Spring Batch创建分区Step时遇'Scope step未激活'错误求助
我在Spring Batch配置里尝试搭建一个能从JobParameters取值的分区Step,代码如下:
@Bean @Qualifier("partitionJob") public Job partitionJob() throws Exception { return jobBuilderFactory .get("partitionJob") .incrementer(new RunIdIncrementer()) .start(partitionStep(null)) // 这里直接传null有问题 .build(); } @Bean @StepScope // 此处报错->Error creating bean public Step partitionStep( @Value("#{jobParameters[gridSize]}") String gridSize) throws Exception { return stepBuilderFactory .get("partitionStep") .partitioner("slaveStep", partitioner()) .gridSize( StringUtils.isEmpty(gridSize) ? 10 : Integer.parseInt(gridSize)) .step(slaveStep(50000)) // 这里直接传固定值,没法从参数取 .taskExecutor(threadPoolTaskExecutor()).build(); } @Bean @StepScope public Step slaveStep(int chunkSize) throws Exception { return stepBuilderFactory .get("slaveStep") .<Person,Person> chunk(chunkSize) .reader(jdbcPagingItemReader()), .writer(csvFileWriterParts()) .listener(stepExecutionListener()).build(); }
我的SpringBoot应用已经加了@EnableBatchProcessing注解。为了在构造Step时获取JobParameters,我用了@StepScope;没有这个注解的示例能正常运行,但没法访问上下文或JobParameters。
给partitionStep加了@StepScope后,出现了这个错误:
Error creating bean with name 'scopedTarget.partitionStep': Scope 'step' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No context holder available for step scope
如果改成JobScope,slaveStep会出现相同的错误。
请问这种场景该用什么正确的作用域?怎么解决这个错误?配置Spring Bean时获取JobParameters的更好方式是什么?
异常堆栈信息:
2018-05-25 21:07:32,075 ERROR [main] org.springframework.batch.core.job.AbstractJob : Encountered fatal error executing job org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.partitionStep': Scope 'step' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No context holder available for step scope at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:361) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197) at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:192) at com.sun.proxy.$Proxy55.getName(Unknown Source) at org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:115) at org.springframework.batch.core.job.AbstractJob.handleStep(AbstractJob.java:392) at org.springframework.batch.core.job.SimpleJob.doExecute(SimpleJob.java:135) at org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:306) at org.springframework.batch.core.launch.support.SimpleJobLauncher$1.run(SimpleJobLauncher.java:135) at org.springframework.core.task.SyncTaskExecutor.execute(SyncTaskExecutor.java:50) at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:128) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333) at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) at org.springframework.batch.core.configuration.annotation.SimpleBatchConfiguration$PassthruAdvice.invoke(SimpleBatchConfiguration.java:127) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213) at com.sun.proxy.$Proxy54.run(Unknown Source) at com.sample.main(ExtractApplication.java:58) Caused by: java.lang.IllegalStateException: No context holder available for step scope at org.springframework.batch.core.scope.StepScope.getContext(StepScope.java:167) at org.springframework.batch.core.scope.StepScope.get(StepScope.java:99) at org.springframework.beans.factory.support.AbstractBeanF
咱们来一步步拆解你的问题:
为什么会报错?
你碰到的核心问题是单例Bean(partitionJob默认是单例)在容器启动时就会被初始化,而你直接调用了partitionStep(null),相当于手动实例化了Step作用域的Bean。但Step作用域只有在Job真正执行的时候才会激活,容器启动阶段根本没有Step上下文,所以就抛出了"Scope 'step' is not active"的异常。
另外你的代码还有两个小坑:
partitionJob里直接调用partitionStep(null),完全绕过了Spring的代理机制,StepScope的特性直接失效;slaveStep的chunkSize写死成了50000,没法从JobParameters动态获取值。
该用什么作用域?
- 分区Step(
partitionStep):用@StepScope是对的,但要正确使用代理注入,不能手动调用; - Slave Step(
slaveStep):如果需要从JobParameters拿参数,同样用@StepScope,通过@Value注入参数,而不是传固定值。
修正后的完整代码示例
1. 先改Job的定义
不要手动调用partitionStep(null),让Spring帮你注入Step代理对象,这样Spring会在Job执行时才真正初始化Step作用域的Bean:
@Bean @Qualifier("partitionJob") public Job partitionJob(Step partitionStep) throws Exception { return jobBuilderFactory .get("partitionJob") .incrementer(new RunIdIncrementer()) .start(partitionStep) // 直接注入代理,不要手动实例化 .build(); }
2. 修正Partition Step
保留@StepScope,用Spring EL直接把gridSize转成Integer,还能加默认值避免空指针,同时注入slaveStep的代理:
@Bean @StepScope public Step partitionStep( @Value("#{jobParameters[gridSize] ?: 10}") Integer gridSize, Step slaveStep) throws Exception { return stepBuilderFactory .get("partitionStep") .partitioner("slaveStep", partitioner()) .gridSize(gridSize) .step(slaveStep) .taskExecutor(threadPoolTaskExecutor()) .build(); }
3. 修正Slave Step
如果需要动态获取chunkSize,同样用@StepScope和@Value从JobParameters取,带默认值更稳妥:
@Bean @StepScope public Step slaveStep( @Value("#{jobParameters[chunkSize] ?: 50000}") Integer chunkSize) throws Exception { return stepBuilderFactory .get("slaveStep") .<Person, Person>chunk(chunkSize) .reader(jdbcPagingItemReader()) .writer(csvFileWriterParts()) .listener(stepExecutionListener()) .build(); }
获取JobParameters的更好方式
除了@Value("#{jobParameters['xxx']}"),还有两种更灵活的玩法:
- 直接注入JobParameters对象:在StepScope的Bean里直接注入
JobParameters,然后按需取值,可读性更好:@Bean @StepScope public Step partitionStep(JobParameters jobParameters) throws Exception { Integer gridSize = jobParameters.getInt("gridSize", 10); // 第二个参数是默认值 // ... 你的业务逻辑 } - JobScope共享全局参数:如果某个参数是整个Job共享的(比如多个Step都要用
gridSize),可以把它抽成@JobScope的Bean,这样Job启动时就初始化,所有Step都能复用:
之后在需要的Step里直接注入这个@Bean @JobScope public Integer gridSize(@Value("#{jobParameters[gridSize] ?: 10}") Integer gridSize) { return gridSize; }Integer就行。
关键提醒
- 绝对不要在单例Bean(比如Job)里直接调用StepScope/JobScope Bean的方法,一定要通过Spring依赖注入拿代理对象;
- StepScope的Bean只有在Step执行时才会被初始化,所以它的依赖也得是延迟加载的(比如用代理);
- 用Spring EL的
?:语法可以轻松加默认值,省去一堆判空代码。
内容的提问来源于stack exchange,提问作者DBreaker

