问题: 当我们使用spring batch 时候,我们使用@jobscope 来参数专递,或者使用@stepscope 来使用step execution 上下文, 我们发现 比较奇怪的错误就会出现: Caused by: java.lang.IllegalStateException: No context holder available for job scope. 我们不禁会问,@jobscope 用错了吗? @jobscope 和 @stepscope 到底再背后做了什么? 为什么会在使用多线程的时候,就会出现这种错误呢?
解决思路:
1. @jobscope, @stepscope 是什么区别?
2. 加上@jobscope, @stepscope,Bean是如何做延迟绑定的?
3. flow 的并行执行,为什么会出现 Caused by: java.lang.IllegalStateException: No context holder available for job scope?
好了,那我们来看看是什么问题。
第一: jobscope, stepscope 区别请看spring batch 的官网,简单理解就是job scope context 和 step scope context
第二: 延迟加载,如何做延迟加载?
其实很简单,咱们去看看spring batch 源码。
private T createLazyProxy(AtomicReference reference, Class type) {
ProxyFactory factory =new ProxyFactory();
factory.setTargetSource(new ReferenceTargetSource<>(reference));
factory.addAdvice(new PassthruAdvice());
factory.setInterfaces(new Class[] { type });
@SuppressWarnings("unchecked")
T proxy = (T) factory.getProxy();
return proxy;
}
咱们去看看set 的 target source 是什么? ReferenceTargetSource extend AbstractLazyCreationTargetSource, 当使用这个对象的时候,调用createObject, 可以获取相应的对象。 从 AbstractLazyCreationTargetSource注解,我们可以了解到。
/**
Useful when you need to pass a reference to some dependency to an object
* but you don't actually want the dependency to be created until it is first used.
* A typical scenario for this is a connection to a remote resource.
**/
private class ReferenceTargetSource extends AbstractLazyCreationTargetSource {
private AtomicReferencereference;
public ReferenceTargetSource(AtomicReference reference) {
this.reference = reference;
}
@Override
protected ObjectcreateObject()throws Exception {
initialize();
return reference.get();
}
}
这里面为什么使用AtomicReferencereference, 请大家看看这个类的使用就明白了。但是我确实觉得延迟加载 有哪里优势,因为我看到每个bean 实际上也是通过代理方式创建了,哦,明白了,这个代理只是将此类的xxx.class 传入代理类, 那个bean根本就没有初始化。等调用的时候,可以通过new instance 将引用注入进去。
很具体的,大家可以搜搜延迟加载怎么实现,应该怎么写延迟加载的应用。应该不难。 好了,咱们继续分析问题在哪里? 我们只要记得 所有bean,加上 @jobscope, @stepscope 之后,都不回加载的,而是等到使用这个bean 的时候,才会去加载,去执行bean 里面的内容。
第三: 为什么使用flow 之后,想并行执行 step的时候,就会出现标题中的错误呢? 下面是简单的flow 配置:
@Bean(name ="singleInterchangeSettlementStatisticJob")
public JobsetlStatJob(
JobBuilderFactory jobBuilderFactory,
Flow splitFlow,
Step ichgStatTaskletMergedStep,
Step ichgSetlStatClearStep) {
return jobBuilderFactory.get(JobEnum.SNGL_ICHG_SETL_STAT.getJobName())
.incrementer(new RunIdIncrementer())
.start(splitFlow)
.next(ichgStatTaskletMergedStep)
.next(ichgSetlStatClearStep)
.build()
.build();
}
/**
* parallel step
*
* @param financeStatFlow
* @param nonFinanceStatFlow
* @return Flow
*/
@Bean
public FlowsplitFlow(Flow financeStatFlow,
Flow nonFinanceStatFlow) {
return new FlowBuilder("ichgStatFlow")
.split(taskExecutor())
.add(financeStatFlow, nonFinanceStatFlow)
.build();
}
/**
* finance Stat Flow
*
* @param snglFinclSetlStatMasterStep
* @return Flow
*/
@JobScope
@Bean
public FlowfinanceStatFlow(Step snglFinclSetlStatMasterStep) {
System.out.println("hhhhhhhhhnonFinanceStatFlowwwwww");
System.out.println(Thread.currentThread().getName());
return new FlowBuilder("financeStatFlow")
.start(snglFinclSetlStatMasterStep)
.build();
}
/**
* non Finance Stat Flow
*
* @param snglNonFinclSetlStatMasterStep
* @return Flow
*/
@JobScope
@Bean
public FlownonFinanceStatFlow(Step snglNonFinclSetlStatMasterStep) {
System.out.println("hhhhhhhhhnonFinanceStatFlow");
System.out.println(Thread.currentThread().getName());
return new FlowBuilder("nonFinanceStatFlow")
.start(snglNonFinclSetlStatMasterStep)
.build();
}
上面的配置,启动不会有问题,但是当调用job 执行的时候,就会出现标题中的问题, 为什么呢? 另外上面的代码中,jobscope 加错了位置也会出现标题中的问题,为什么呢?
第一: 我们通过错误技术栈,我们知道,报错是 getContext, 出现null 的异常。
第二: 我们掉进去 进入getContext,我们知道 里面的实现是使用 thread local
第三: 正常情况下不会出现标题上的问题,是因为JobSynchronizationManager.register(), 的调用。
那么我们如何避免上面的问题呢?
首先: 使用延迟加载的方式,让bean(方法上有jobscope, stepscope) 不去加载。所以@jobscope, stepscope 一定要注意位置
其次: 并行执行step时候,为flow加一个 executor,这个executor就是为了将该线程注入jobExecution的。
比如,如果将 @jobscope, 放到splitFlow 方法上,启动就会报错,因为 singleInterchangeSettlementStatisticJob 这个bean 加载 splitFlow bean 的时候,会去取jobExecution 上下文,这个时候是为null, 会报错。
那么这个 executor 怎么写呢? 这个就很简单,
@Bean
public SimpleAsyncTaskExecutortaskExecutor() {
return new SimpleAsyncTaskExecutor() {
@Override
protected void doExecute(Runnable task) {
//gets the jobExecution of the configuration thread
JobExecution jobExecution = JobSynchronizationManager.getContext().getJobExecution();
super.doExecute(new Runnable() {
@Override
public void run() {
JobSynchronizationManager.register(jobExecution);
try {
task.run();
}finally {
JobSynchronizationManager.release();
}
}
});
}
};
}
有人可能会问 financeStatFlow 和 nonFinanceStatFlow 为什么不会出现异常呢? 它们也加了jobscope 注解,这个是因为.split(taskExecutor()), 这个使得 financeStatFlow 和 nonFinanceStatFlow不会加载的,所以不会创建这个bean, 所以没有上下文的问题。 等需要创建这个bean 的时候,上面的方法已经初始化JobExecution 上线文了。 所以不会有问题。
这个是spring batch的一个bug,网上有很多都争议关于这个话题,spring batch 下面个版本说有解决,我们拭目以待把,大家有现在有遇到这个问题的,欢迎留言,一起解决。我这边是可以解决的,然后debug 大概知道spring batch 里面怎么玩的。欢迎多交流交流!