系統初始化器
一、系統初始化器介紹
我們知道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是實現,如果是多個實現則用,分隔
我們在第一篇文章中說過
框架初始化分爲:
- 配置資源加載器
- 配置primarySources(一般是我們的啓動類)
- 應用環境的檢測(springboot1.x版本有兩種環境,標準環境和web環境,spingboot2.x添加了一種Reactive環境)
- 配置系統初始化器
- 配置應用監聽器
- 配置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的功能準備了基礎。