SpringBatch從入門到精通-2-StepScope作用域和用法

語言: CN / TW / HK

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預設是單例的。 程式碼位置: http://github.com/jackssybin/jackssybin_springBatch.git

    本文由部落格一文多發平臺 OpenWrite 釋出!