【Springboot深入解析】系統初始化器

系統初始化器

一、系統初始化器介紹

我們知道Spring 是一個擴展性很強的容器框架,爲開發者提供了豐富的擴展入口,其中一個擴展點便是ApplicationContextInitializer (應用上下文初始化器 或者 系統初始化器)。

ApplicationContextInitializer 是 Spring 在執行 ConfigurableApplicationContext.refresh() 方法對應用上下文進行刷新之前調用的一個回調接口,用來完成對 Spring 應用上下文個性化的初始化工作,該接口定義在 org.springframework.context 包中,其內部僅包含一個 initialize() 方法

官方對其描述是 Spring容器刷新之前執行的一個回調函數,它的作用是向 Springboot容器中註冊屬性

使用的話,可以繼承接口自定義實現,我們先認識一下它能呈現給我們的效果。


下面通過系統初始化器向springboot容器中注入屬性的方式,

方法一:

初始化一個springboot項目之後,我們創建initializer的包,裏面定義了一個自定義系統初始化器。該類繼承了ApplicationContextInitializer,參數類型是ConfigurableApplicationContext 。

ConfigurableApplicationContext 接口的作用就是在ApplicationContext的基礎上增加了一系列配置應用上下文的功能。

import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

import java.util.HashMap;
import java.util.Map;

@Order(1) // 使用order屬性,設置該類在spring容器中的加載順序,值越小優先級越高
public class FirstInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        // 獲取環境
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        // 自定義一個屬性
        Map<String, Object> map = new HashMap<>();
        map.put("chenxiao","gogo");
        MapPropertySource mapPropertySource = new MapPropertySource("firstInitializer", map);
        // 加入環境的屬性集中
        environment.getPropertySources().addLast(mapPropertySource);
        System.out.println("run FirstInitializer");
    }
}

之後我們在resource目錄下創建一個META-INF,裏面創建一個文件spring.factories(配置文件),配置信息是系統初始化器的路徑。

org.springframework.context.ApplicationContextInitializer=com.example.demo.Initializer.FirstInitializer

接下來爲了驗證效果,我們創建一個service

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@service
public class TestService implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public String test() {
        // 返回上下文當中的環境變量中的屬性
        return applicationContext.getEnvironment().getProperty("chenxiao");
    }
}

再創建一個controller,調用service的方法:

import com.example.demo.service.TestService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @Autowired
    private TestService testService;

    @GetMapping("test")
    public String test() {
        return testService.test();
    }
}

啓動springboot之後

com.example.demo.DemoApplication

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.1.RELEASE)

run FirstInitializer
2020-03-28 10:34:55.270  INFO 19092 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on DESKTOP-Q10BPSI with PID 19092 (D:\CodePractise\springbootLearn\target\classes started by JunQiao Lv in D:\CodePractise\springbootLearn)
...

可以看到打印的信息“run FirstInitializer”。用postman進行測試:
在這裏插入圖片描述
獲取到了我們想要的結果。

方法二:

創建SecondInitializer

import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

import java.util.HashMap;
import java.util.Map;

@Order(2)
public class SecondInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        Map<String, Object> map = new HashMap<>();
        map.put("chenxiao2", "gogo2");
        MapPropertySource mapPropertySource = new MapPropertySource("secondInitializer", map);
        environment.getPropertySources().addLast(mapPropertySource);
        System.out.println("run secondInitializer");
    }
}

更改啓動類

import com.example.demo.Initializer.SecondInitializer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
//        SpringApplication.run(DemoApplication.class, args);
        SpringApplication springApplication = new SpringApplication(DemoApplication.class);
        springApplication.addInitializers(new SecondInitializer());
        springApplication.run(args);
    }
}

更改service方法

 public String test() {
        // 返回上下文當中的環境變量中的屬性
        return applicationContext.getEnvironment().getProperty("chenxiao2");
}

啓動springboot

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.1.RELEASE)

run FirstInitializer
run secondInitializer
2020-03-28 11:05:03.732  INFO 19808 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on DESKTOP-Q10BPSI with PID 19808 (D:\CodePractise\springbootLearn\target\classes started by JunQiao Lv in D:\CodePractise\springbootLearn)
...

postman進行測試
在這裏插入圖片描述
方法三:

創建一個ThirdInitializer

import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;

import java.util.HashMap;
import java.util.Map;

@Order(3)
public class ThirdInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        Map<String, Object> map = new HashMap<>();
        map.put("chenxiao3", "gogo3");
        MapPropertySource mapPropertySource = new MapPropertySource("thirdInitializer", map);
        environment.getPropertySources().addLast(mapPropertySource);
        System.out.println("run thirdInitializer");
    }
}

這次我們在application.properties文件中添加自定義系統初始器的位置

context.initializer.classes=com.example.demo.Initializer.ThirdInitializer

更改service方法

 public String test() {
        // 返回上下文當中的環境變量中的屬性
        return applicationContext.getEnvironment().getProperty("chenxiao3");
}

啓動springboot。

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.1.RELEASE)

run thirdInitializer
run FirstInitializer
run secondInitializer
2020-03-28 11:07:03.732  INFO 19808 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on DESKTOP-Q10BPSI with PID 19808 (D:\CodePractise\springbootLearn\target\classes started by JunQiao Lv in D:\CodePractise\springbootLearn)
...

我們發現 “run thirdInitializer”竟然在最前面,我們後面會說這個

看一下測試結果
在這裏插入圖片描述

以上通過三種方式利用系統初始化器向 Springboot容器中註冊屬性。

實現方式一

  • 實現 ApplicationContextInitializer接口
  • spring.factories內填寫接口實現
  • 填寫的key爲org.springframework.context.ApplicationContextInitializer

實現方式二

  • 實現 ApplicationContextInitializer接口
  • SpringApplication類初始後設置進去

實現方式三

  • 實現 ApplicationContextinitializer接口
  • application. properties內填寫接口實現
  • 填寫的key爲context.initializer.classes

我們知道Order值越小越先執行,但是application properties中定義的卻更優先。下面開始扣原理了,看一下我們定義的系統初始化器是如何被springboot容器所識別並加載到容器中的。

二、SpringFactoriesLoader介紹

上述說的實現的關鍵是SpringFactoriesLoader,下面是官方給的介紹。
在這裏插入圖片描述
意思是:

  • 框架內部使用的通用工廠加載機制
  • 從classpath下多個jar包特定的位置讀取文件並初始化類
  • 文件內容須是k-v形式,即properties類型
  • key是全限定名(抽象類|接口)、value是實現,如果是多個實現則用,分隔

我們在第一篇文章中說過

框架初始化分爲:

  1. 配置資源加載器
  2. 配置primarySources(一般是我們的啓動類)
  3. 應用環境的檢測(springboot1.x版本有兩種環境,標準環境和web環境,spingboot2.x添加了一種Reactive環境)
  4. 配置系統初始化器
  5. 配置應用監聽器
  6. 配置main方法所在類

好,我們看一下源碼,探究系統初始化器是如何被springboot發現的

我們一層一層往下剖
在這裏插入圖片描述
調用的是這個run方法
在這裏插入圖片描述
繼續,發現這裏初始化一個springApplication的實例
在這裏插入圖片描述
進入SpringApplication的構造方法
在這裏插入圖片描述
看到這裏發現 系統初始化器的註冊。具體是通過getSpringFactoriesInstances(ApplicationContextInitializer.class)方法進行的一個系統初始化器的實現,繼續挖。
在這裏插入圖片描述
這裏調用了一個同名方法,繼續
在這裏插入圖片描述
方法中首先獲取一個類加載器,下面通過SpringFactoriesLoader.loadFactoryNames(type, classLoader)方法獲得所有的類的全限定名。

createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names) 通過獲取到的全限定名創建相應的實例。

接着對這些實例進行一個排序AnnotationAwareOrderComparator.sort(instances);

最後返回這些實例。

下面依次看這三個方法:

首先是pringFactoriesLoader.loadFactoryNames(type, classLoader)。
在這裏插入圖片描述

在這裏插入圖片描述

這裏首先獲得工廠類的名字,繼續往下
在這裏插入圖片描述
類似的,我們看一下springboot容器中META-INF目錄下的spring.factories
在這裏插入圖片描述
這裏有好多個系統初始化器
在這裏插入圖片描述
看一下斷點處,可以找到我們定義的FirstInitializer。
在這裏插入圖片描述
下面開始調用getOrDefault方法,沒有key的話,則返回空集合。
在這裏插入圖片描述
回退到上一級,我們下面介紹第二個方法createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);這個方法會爲它們創造實例。

(name爲系統初始化容器哈)
在這裏插入圖片描述
進入createSpringFactoriesInstances方法
在這裏插入圖片描述
實例都初始化結束後,返回上一級
在這裏插入圖片描述
接着看第三個方法AnnotationAwareOrderComparator.sort(instances);,他負責對實例集合進行一個排序(通過order中的值),值越小越排在前面。

接下來就是返回實例集合,然後完成註冊。
在這裏插入圖片描述
看一下set方法

public void setInitializers(Collection<? extends ApplicationContextInitializer<?>> initializers) {
        this.initializers = new ArrayList(initializers);
    }

這就是完整的系統初始化器從被發現,並被初始化,以及到被註冊到SpringApplication實例的過程。

核心是SpringFactoriesloader作用:Spring Boot框架中從類路徑jar包中讀取特定文件實現擴展類的載入。

在這裏插入圖片描述

三、系統初始化器原理

下面探究一下系統初始化器是如何被調用的以及被調用的原理。

系統初始化器接口的官方註釋:
在這裏插入圖片描述
它的描述含義大致如下:

  • 上下文刷新即spring的refresh方法前調用
  • 用來編碼設置一些屬性變量通常用在web環境中
  • 可以通過order接口進行排序

在第一篇文章中,我們過了springboot大體的流程,其中在準備上下文過程中,會遍歷調用Initalizer的initalize方法,我們之前自定義實現過
在這裏插入圖片描述
我們在run方法內看一下源碼
在這裏插入圖片描述
進去之後
在這裏插入圖片描述
我們進入準備上下文的方法prepareContext中,可以看到調用初始化器部分。
在這裏插入圖片描述
進入,發現一目瞭然,這裏進行了迭代調用系統初始化器的初始化方法。( getInitializers返回所有的系統初始化器)
在這裏插入圖片描述
下面看一下最初系統初始化器實現方式二的情況:

    public static void main(String[] args) {
//        SpringApplication.run(DemoApplication.class, args);
        SpringApplication springApplication = new SpringApplication(DemoApplication.class);
        springApplication.addInitializers(new SecondInitializer());
        springApplication.run(args);
    }

我們通過new一個SpringApplication,然後添加我們的初始化器。

我們知道SpringApplication初始化之後,系統初始化器已經設置過了。
在這裏插入圖片描述
但是SpingApplication實例提供了addInitializers(new SecondInitializer())方法來幫助我們增加自定義的初始化器
在這裏插入圖片描述
然後纔是springApplication.run(args);的run方法 。和方式一的run方法是同一個。

        SpringApplication springApplication = new SpringApplication(DemoApplication.class);
        springApplication.addInitializers(new SecondInitializer());
        springApplication.run(args);

然後就能保證添加的系統初始化器能夠在後面的run方法中正常執行。

下面看一下第三種方式,我們是通過在application.properties 文件中添加配context.initializer.classes=com.example.demo.initializer.ThirdInitializer來實現的。

這個主要是通過DelegatingApplicationContextInitializer初始化器來實現的這個類DelegatingApplicationContextInitializer定義在SpringBoot中

這種實現方式主要通過入DelegatingApplicationContextInitializer類來完成的。可以看到DelegatingApplicationContextInitializer裏的order=0。這個初始化器最先被調到。
在這裏插入圖片描述
在spring的springFactories中有這個類初始化器,這個在加載系統類初始化器的時候被加載。
在這裏插入圖片描述

回顧整個系統初始化器,大致就是這個流程。
在這裏插入圖片描述

三種實現初始化器的實現原理:

  • 定義在spring.factories 文件中被SpringFactoriesLoader發現註冊
  • 初始化完畢手動添加
  • 定義成環境變量被DelegatingApplicationContextInitializer發現註冊

搞清楚這個,你就瞭解Spring 應用上下文個性化的初始化工作是如何進行的,以及爲你今後想拓展Spring的功能準備了基礎。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章