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应用中才有意义
img

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
image-20220528234144831

看出stepScope实现了BeanFactoryPostProcessor。引入了bpp即实例化之前做扩展。这里是讲stepScope给注册到了spring容器中

image-20220529002656996

获取bean的时候AbstractBeanFactory#doGetBean。判断bean不是mbd.isSingleton(),不是mbd.isPrototype(),若是自定义的即

mbd.getScope()。

那是如何获取的呢StepScope#get(String, ObjectFactory) ->这里面会获取StepContext内容。

image-20220529002838153

stepContext=getContext() ->这里面其实是直接从ThreadLocal中获取内容。获取stepContext(这里面从有jobParam,stepContext)

image-20220529002950802

那我们可以看到这个StepSynchronizationManager已经有获取的。那什么时候注册进去的呢?

image-20220529003544898

AbstractStep#execute中在step的抽象类里面。执行doExecute(stepExecution);之前的方法doExecutionRegistration

image-20220529003805983

这里面将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注解的注入功能提供支持。下面是调用链

img

这里先简单介绍一下图上的几个类的作用。

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<Map<String, Object>> threadLoacal = new ThreadLocal<Map<String, Object>>() {
        @Override
        protected Map<String, Object> initialValue() {
            return new HashMap<String, Object>();
        }
    };

    public Object get(String name, ObjectFactory<?> objectFactory) {
        Map<String, Object> 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<String, Object> 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默认是单例的。
    代码位置:
    https://github.com/jackssybin/jackssybin_springBatch.git

本文由博客一文多发平台 OpenWrite 发布!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容