乐趣区

关于java:SpringBatch从入门到精通2StepScope作用域和用法

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

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

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

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

mbd.getScope()。

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

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

那咱们能够看到这个 StepSynchronizationManager 曾经有获取的。那什么时候注册进去的呢?

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

这外面将 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注解的注入性能提供反对。上面是调用链

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

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…

退出移动版