問題:? 當我們使用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 里面怎么玩的。歡迎多交流交流!