spring batch @jobscope: No context holder available

问题:  当我们使用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 里面怎么玩的。欢迎多交流交流!

你可能感兴趣的:(spring batch @jobscope: No context holder available)