SpringBatch从入门到精通-2-StepScope作用域和用法

1.StepSope 是一种scope
在此之前,先说一下IOC容器中几种bean的作用范围:

  • singleton单例模式 – 全局有且仅有一个实例
  • prototype原型模式 – 每次获取Bean的时候会有一个新的实例
  • request – request表示该针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP request内有效
  • session – session作用域表示该针对每一次HTTP请求都会产生一个新的bean,同时该bean仅在当前HTTP session内有效
  • globalsession – global session作用域类似于标准的HTTP Session作用域,不过它仅仅在基于portlet的web应用中才有意义
SpringBatch从入门到精通-2-StepScope作用域和用法
文章图片

2.StepSope 是一种自定义step
目的是在每一次启动Job实例时底层单位(RPW或者也可是step)的参数可以灵活修改或者达到可变,因为一个系统里可能有多个job实例,每个job实例对应的参数是不一样的。在step运行期间需要获取的job参数/stepContext/jobContext,因此需要自定义一个作用域,令其与Step的生命周期一致。
使用注解了。那么stepScope修饰的一定是一个@Bean
3.如何使用。@Value是支持spel表达式的
3.1 大部分场景是Spel 表达式。在底层reader/process/writer 中使用@Value获取jobParamter/stepContext/jobContext
  • job参数:\#{jobParameters[xy]}
  • job运行上下文:#{jobExecutionContext[xy]}
  • step运行上下文:#{stepExecutionContext[xy]}
3.2 SpEL引用bean
  • bean对象:\#{car}
  • bean对象属性:\#{car.brand}
  • bean对象方法:\#{car.toString()}
  • 静态方法属性:\#{T(java.lang.Math).PI}
3.3 系统属性
  • 系统变量:systemProperties
  • 环境变量:#{systemEnvironment['HOME']}
3.4 运算符号
  • if-else 运算符(三目运算符 ?:(temary), ?:(Elvis))
  • 比较运算符(< , > , == , >= , <= , lt , gt , eg , le , ge)
  • 逻辑运算符(and , or , not , |)
  • 正则表达式(#{admin.email matches ‘[a-zA-Z0-9._%±]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,4}’})
  • 算术运算符(+,-,*,/,%,^(加号还可以用作字符串连接))
4.可能遇到问题
  1. 问题: Scope 'step' is not active for the current thread;
Error creating bean with name 'scopedTarget.demo01StepScopeStep': 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

原因:看代码
@Bean public Job demo01StepScopeJob(){ return jobBuilderFactory.get("demo01StepScopeJob") .start(demo01StepScopeStepError(null)) .build(); } /** ** 这个时候在 Step (demo01StepScopeStep) 中添加注解stepscope。 ** Scope 'step' is not active for the current thread. 这个说的也很明白,step还没有装载初始化完成呢。 ** 所以只有在step激活,即装载成功之后才能获取@Value 这种情况。我们可以把taskle 定义成个bean来获取 **/ @Bean @StepScope public Step demo01StepScopeStepError(@Value("${demo01.param.name}") String paramName){ return stepBuilderFactory.get("demo01StepScopeStep") .tasklet(new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { logger.info("=======demo01StepScopeStep======paramName:{}",paramName); return null; } }).build(); }// 改造如下@Bean public Job demo01StepScopeJob(){ return jobBuilderFactory.get("demo01StepScopeJob") .start(demo01StepScopeStep()) .build(); }//这个时候step上面的bean可以不要。因为stepScope只需要定义需要获取@Value的 public Step demo01StepScopeStep(){ return stepBuilderFactory.get("demo01StepScopeStep") .tasklet(demo01StepScopeTasklet(null)) .build(); }@Bean @StepScope//这里的@StepScope 可以有也可以不用。因为@value只是在application.properties中的内容 public Tasklet demo01StepScopeTasklet(@Value("${demo01.param.name}") String paramName){ return new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { logger.info("=======demo01StepScopeStep======paramName:{}",paramName); return null; } }; }

这里面获取的是配置文件的内容。不是step内的特定内容。所以可以直接使用@Value在参数bean里。也可以直接在configuration内。
若使用jobParam参数内的@Value呢?那必须使@StepScope
@Bean @StepScope public Tasklet demo01StepScopeTasklet(@Value("#{jobParameters[rnd]} ") String rnd){ return new Tasklet() { @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { logger.info("=======demo01StepScopeTasklet======rnd:{}",rnd); return null; } }; }

5.StepScope原理
org.springframework.batch.core.scope.StepScope.java

SpringBatch从入门到精通-2-StepScope作用域和用法
文章图片

看出stepScope实现了BeanFactoryPostProcessor。引入了bpp即实例化之前做扩展。这里是讲stepScope给注册到了spring容器中
SpringBatch从入门到精通-2-StepScope作用域和用法
文章图片

获取bean的时候AbstractBeanFactory#doGetBean。判断bean不是mbd.isSingleton(),不是mbd.isPrototype(),若是自定义的即
mbd.getScope()。
那是如何获取的呢StepScope#get(String, ObjectFactory) ->这里面会获取StepContext内容。
SpringBatch从入门到精通-2-StepScope作用域和用法
文章图片

stepContext=getContext() ->这里面其实是直接从ThreadLocal中获取内容。获取stepContext(这里面从有jobParam,stepContext)
SpringBatch从入门到精通-2-StepScope作用域和用法
文章图片

那我们可以看到这个StepSynchronizationManager已经有获取的。那什么时候注册进去的呢?
SpringBatch从入门到精通-2-StepScope作用域和用法
文章图片

AbstractStep#execute中在step的抽象类里面。执行doExecute(stepExecution); 之前的方法doExecutionRegistration
SpringBatch从入门到精通-2-StepScope作用域和用法
文章图片

这里面将stepExecution添加进ThreadLocal中。这里面可以认为是threadlocal(其实里面是一个栈可以存多个stepExecution!!!,兼容step套step那种的。)
其实这里已经能解答了。但还有个问题。@Value是如何从stepExecution中获取jobParm/stepContext/jobContext的
额外(通过@value是如何在那个阶段获取值的呢):
springboot启动过程中,有两个比较重要的过程,如下:
1 扫描,解析容器中的bean注册到beanFactory上去,就像是信息登记一样。
2 实例化、初始化这些扫描到的bean。
@Value的解析就是在第二个阶段。BeanPostProcessor定义了bean初始化前后用户可以对bean进行操作的接口方法,它的一个重要实现类AutowiredAnnotationBeanPostProcessor正如javadoc所说的那样,为bean中的@Autowired@Value注解的注入功能提供支持。下面是调用链
SpringBatch从入门到精通-2-StepScope作用域和用法
文章图片

这里先简单介绍一下图上的几个类的作用。
AbstractAutowireCapableBeanFactory: 提供了bean创建,属性填充,自动装配,初始胡。支持自动装配构造函数,属性按名称和类型装配。实现了AutowireCapableBeanFactory接口定义的createBean方法。
AutowiredAnnotationBeanPostProcessor: 装配bean中使用注解标注的成员变量,setter方法, 任意的配置方法。比较典型的是@Autowired注解和@Value注解。
InjectionMetadata: 类的注入元数据,可能是类的方法或属性等,在AutowiredAnnotationBeanPostProcessor类中被使用。
AutowiredFieldElement: 是AutowiredAnnotationBeanPostProcessor的一个私有内部类,继承InjectionMetadata.InjectedElement,描述注解的字段。
StringValueResolver: 一个定义了处置字符串值的接口,只有一个接口方法resolveStringValue,可以用来解决占位符字符串。本文中的主要实现类在PropertySourcesPlaceholderConfigurer#processProperties方法中通过lamda表达式定义的。供ConfigurableBeanFactory类使用。
PropertySourcesPropertyResolver: 属性资源处理器,主要功能是获取PropertySources属性资源中的配置键值对。
PropertyPlaceholderHelper: 一个工具类,用来处理带有占位符的字符串。形如${name}的字符串在该工具类的帮助下,可以被用户提供的值所替代。替代途经可能通过Properties实例或者PlaceholderResolver(内部定义的接口)。
PropertyPlaceholderConfigurerResolver: 上一行所说的PlaceholderResolver接口的一个实现类,是PropertyPlaceholderConfigurer类的一个私有内部类。实现方法resolvePlaceholder中调用了外部类的resolvePlaceholder方法。
6.自定义一个scope
定义JakcssybinScope
/** * 定义在同一个线程内,多次获取同一个bean 获取的是同一个。 * 如果不用该注解 获取的是不同的bean */ public class JackssybinScope implements Scope {private final ThreadLocal threadLoacal = new ThreadLocal() { @Override protected Map initialValue() { return new HashMap(); } }; public Object get(String name, ObjectFactory objectFactory) { Map scope = threadLoacal.get(); Object obj = scope.get(name); // 不存在则放入ThreadLocal if (obj == null) { obj = objectFactory.getObject(); scope.put(name, obj); System.out.println("Not exists " + name + "; hashCode: " + obj.hashCode()); } else { System.out.println("Exists " + name + "; hashCode: " + obj.hashCode()); }return obj; }public Object remove(String name) { Map scope = threadLoacal.get(); return scope.remove(name); }public String getConversationId() { return null; }public void registerDestructionCallback(String arg0, Runnable arg1) { }public Object resolveContextualObject(String arg0) { return null; }}

注入jackssybinScope
@Component public class JackssybinBPP implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { System.out.println("==============注册JackssybinBPP========="); beanFactory.registerScope("jackssybinScope", new JackssybinScope()); } }

引用
@Service @Scope("jackssybinScope") public class JackssybinHaveScopeService {public String getMessage() { return "Hello World!"+this.hashCode(); } }

未引用
@Service public class JackssybinNoScopeService {public String getMessage() { return "Hello World!"+this.hashCode(); } }

测试
@SpringBootTest(classes = {Demo01StepScopeApplication.class}) public class JackssybinScopeTest {@Autowired ApplicationContext ctx; @Test public void jackssybinScopetest2(){ JackssybinHaveScopeService service = ctx.getBean(JackssybinHaveScopeService.class); System.out.println(service.getMessage()+"="+service.hashCode()); JackssybinHaveScopeService service2= ctx.getBean(JackssybinHaveScopeService.class); System.out.println(service2.getMessage()+"="+service2.hashCode()); System.out.println("======================"); JackssybinNoScopeService service3 = ctx.getBean(JackssybinNoScopeService.class); System.out.println(service3.getMessage()+"="+service3.hashCode()); JackssybinNoScopeService service4= ctx.getBean(JackssybinNoScopeService.class); System.out.println(service4.getMessage()+"="+service4.hashCode()); } }

结果
Not exists jackssybinHaveScopeService; hashCode: 1842102517 Hello World!1842102517=1842102517 Exists jackssybinHaveScopeService; hashCode: 1842102517 Hello World!1842102517=1842102517 ====================== Hello World!728236551=728236551 Hello World!728236551=728236551

结论:
  1. jackssybinScope生效了。
  2. ctx中的bean默认是单例的。
代码位置:
【SpringBatch从入门到精通-2-StepScope作用域和用法】https://github.com/jackssybin...

    推荐阅读