SpringBatch desde la entrada hasta el dominio-2-Alcance y uso de StepScope

1. StepSope es un alcance

Antes de eso, hablemos del alcance de varios beans en el contenedor IOC:

  • patrón singleton singleton: solo hay una instancia a nivel mundial

  • prototipo patrón de prototipo: cada vez que obtenga un bean, habrá una nueva instancia

  • solicitud: la solicitud indica que se generará un nuevo bean para cada solicitud HTTP, y el bean solo es válido dentro de la solicitud HTTP actual

  • sesión: el alcance de la sesión indica que se generará un nuevo bean para cada solicitud HTTP, y el bean solo es válido dentro de la sesión HTTP actual

  • sesión global: el alcance de la sesión global es similar al alcance de la sesión HTTP estándar, pero solo tiene sentido en las aplicaciones web basadas en portlets.

imagen

2.StepSope es un paso personalizado

El propósito es que los parámetros de la unidad subyacente (RPW o paso) se puedan modificar o variar de forma flexible cada vez que se inicia una instancia de trabajo, ya que puede haber varias instancias de trabajo en un sistema y los parámetros correspondientes a cada instancia de trabajo son diferentes. . . El parámetro de trabajo /stepContext/jobContext que debe obtenerse durante la operación del paso, por lo que debe personalizar un ámbito para que sea coherente con el ciclo de vida del paso.

Usa anotaciones. Entonces stepScope debe ser modificado por un @Bean

3. Cómo usar. @Value es compatible con expresiones ortográficas

3.1 La mayoría de los escenarios son expresiones ortográficas. Obtenga jobParamter/stepContext/jobContext usando @Value en el lector/proceso/escritor subyacente
  • parámetros del trabajo: #{jobParameters[xy]}

  • contexto de ejecución del trabajo: #{jobExecutionContext[xy]}

  • contexto de ejecución del paso: #{stepExecutionContext[xy]}

3.2 Frijoles de referencia SpEL
  • objeto de frijol: #{coche}

  • propiedad del objeto bean: #{car.brand}

  • método de objeto de frijol: #{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
复制代码

imagen-20220528234144831

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

imagen-20220529002656996

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

mbd.getScope()。

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

imagen-20220529002838153

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

imagen-20220529002950802

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

imagen-20220529003544898

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

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

imagen

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

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. Personaliza un alcance

Definir 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;
    }

}
复制代码

inyectar jackssybinScope

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

cotizar

@Service
@Scope("jackssybinScope")
public class JackssybinHaveScopeService {

    public String getMessage() {
        return "Hello World!"+this.hashCode();
    }
}
复制代码

no citado

@Service
public class JackssybinNoScopeService {

    public String getMessage() {
        return "Hello World!"+this.hashCode();
    }
}
复制代码

prueba

@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());
    }
}
复制代码

resultado

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
复制代码

En conclusión:

  1. jackssybinScope entra en vigor.
  2. Los beans en ctx son singleton por defecto.

Ubicación del código: github.com/jackssybin/…

¡Este artículo es publicado por OpenWrite , una plataforma de blogs de publicaciones múltiples !

Supongo que te gusta

Origin juejin.im/post/7102846252642992159
Recomendado
Clasificación