一、前言
在spring時代配置檔案的載入都是通過web.xml配置載入的(Servlet3.0之前),可能配置方式有所不同,但是大多數都是通過指定路徑的檔名的形式去告訴spring該載入哪個檔案;
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/application*.xml</param-value>
</context-param>
而到了springboot時代,我們發現原來熟悉的web.xml已不復存在,但是springboot卻依然可以找到預設的配置檔案(application.yml),那它是如何實現的呢?今天我們就一起來探究一下springboot自動載入配置檔案的機制!
看完本篇文章你將瞭解到:
- springboot什麼時候載入配置檔案
- springboot通過哪個類載入配置檔案
- springboot自動載入配置檔案流程
- 啟用檔案優先順序
- 檔案載入路徑優先順序
- 檔案字尾優先順序
二、提出猜想
我們知道在使用springboot中我們只要在resources下面新建一個application.yml檔案他就會自動載入,那是不是springboot預設在哪裡配置了這個路徑和檔名?
三、驗證猜想
為了證實我們的猜想,我們可以通過檢視springboot專案原始碼,跟著debug一步一步走;
這裡我使用的是springboot2.0版本,2.0與1.5版本比較啟動的大體流程是一樣的,只不過在一些實現中有所差異;
1.啟動流程
要知道springboot如何載入配置檔案,就需要了解它的啟動流程:
我們從main方法進入,大概的呼叫流程如下:
DemoApplication.main->SpringApplication.run->new SpringApplication().run
其實啟動的主要過程都在new SpringApplication().run();
- new SpringApplication():建立SpringApplication例項,負責載入配置一些基本的環境變數、資源、構造器、監聽器
- run():負責springboot整個啟動過程,包括載入建立環境、列印banner、配置檔案、配置應用上下文,載入bean等等sb整個生命週期幾乎都在run方法中;
今天我們的主題是sb如何載入配置檔案,所以著重講解載入配置檔案和之前的操作原理和原始碼,其他的功能以後有機會再和大家一起研究,下面我們來看看new SpringApplication()
做了什麼操作;
2.建立SpringApplication例項
/**
* 建立一個SpringApplication實體,應用程式上下文將從指定的主源文件載入bean以獲取詳細資訊,
* 這個例項可以在呼叫之前自定義
* @param resourceLoader
* @param primarySources
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
//使用的資源載入器
this.resourceLoader = resourceLoader;
//主要的bean資源 primarySources【在這裡是啟動類所在的.class】,不能為null,如果為null,拋異常
Assert.notNull(primarySources, "PrimarySources must not be null");
//啟動類的例項陣列轉化成list,放在LinkedHashSet集合中
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
/**
* 建立應用型別,不同應用程式型別,建立不同的環境
* springboot1.5 只有兩種型別:web環境和非web環境
* springboot2.0 有三種應用型別:WebApplicationType
* NONE:不需要再web容器的環境下執行,也就是普通的工程
* SERVLET:基於servlet的Web專案
* REACTIVE:響應式web應用reactive web Spring5版本的新特性
*/
this.webApplicationType = WebApplicationType.deduceFromClasspath();
/**
* 每一個initailizer都是一個實現了ApplicationContextInitializer介面的例項。
* ApplicationContextInitializer是Spring IOC容器中提供的一個介面: void initialize(C applicationContext);
* 這個方法它會在ConfigurableApplicationContext的refresh()方法呼叫之前被呼叫(prepareContext方法中呼叫),
* 做一些容器的初始化工作。
*/
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
/**
* Springboot整個生命週期在完成一個階段的時候都會通過事件推送器(EventPublishingRunListener)產生一個事件(ApplicationEvent),
* 然後再遍歷每個監聽器(ApplicationListener)以匹配事件物件,這是一種典型的觀察者設計模式的實現
* 具體事件推送原理請看:sb事件推送機制圖
*/
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 指定main函式啟動所在的類,即啟動類BootApplication.class
this.mainApplicationClass = deduceMainApplicationClass();
}
我們來大概的看下ApplicationListener的一些實現類以及他們具體的功能簡介
這些監聽器的實現類都是在spring.factories檔案中配置好的,程式碼中通過getSpringFactoriesInstances
方法獲取,這種機制叫做SPI機制:通過本地的註冊發現獲取到具體的實現類,輕鬆可插拔。
SpringBoot預設情況下提供了兩個spring.factories檔案,分別是:
spring-boot-2.0.2.RELEASE.jar
spring-boot-autoconfigure-2.0.2.RELEASE.jar
概括來說在建立SpringApplication例項的時候,sb會載入一些初始化和啟動的引數與類,如同跑步比賽時的等待發令槍的階段;
3.run方法
(1)、事件推送原理
SB啟動過程中分多個階段或者說是多個步驟,每完成一步就會產生一個事件,並呼叫對應事件的監聽器,這是一種標準的觀察者模式,這在啟動的過程中有很好的擴充套件性,下面我們來看看sb的事件推送原理:
SpringBoot事件推送原理圖:
(2)、run方法整體流程簡述
/**
* 執行應用程式,建立並重新整理一個新的應用程式上下文
*
* @param args
* @return
*/
public ConfigurableApplicationContext run(String... args) {
/**
* StopWatch: 簡單的秒錶,允許定時的一些任務,公開每個指定任務的總執行時間和執行時間。
* 這個物件的設計不是執行緒安全的,沒有使用同步。SpringApplication是在單執行緒環境下,使用安全。
*/
StopWatch stopWatch = new StopWatch();
// 設定當前啟動的時間為系統時間startTimeMillis = System.currentTimeMillis();
stopWatch.start();
// 建立一個應用上下文引用
ConfigurableApplicationContext context = null;
// 異常收集,報告啟動異常
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
/**
* 系統設定headless模式(一種缺乏顯示裝置、鍵盤或滑鼠的環境下,比如伺服器),
* 通過屬性:java.awt.headless=true控制
*/
configureHeadlessProperty();
/*
* 獲取事件推送監器,負責產生事件,並呼叫支某類持事件的監聽器
* 事件推送原理看上面的事件推送原理圖
*/
SpringApplicationRunListeners listeners = getRunListeners(args);
/**
* 釋出一個啟動事件(ApplicationStartingEvent),通過上述方法呼叫支援此事件的監聽器
*/
listeners.starting();
try {
// 提供對用於執行SpringApplication的引數的訪問。取預設實現
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
/**
* 構建容器環境,這裡載入配置檔案
*/
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 對環境中一些bean忽略配置
configureIgnoreBeanInfo(environment);
// 日誌控制檯列印設定
Banner printedBanner = printBanner(environment);
// 建立容器
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context);
/**
* 準備應用程式上下文
* 追蹤原始碼prepareContext()進去我們可以發現容器準備階段做了下面的事情:
* 容器設定配置環境,並且監聽容器,初始化容器,記錄啟動日誌,
* 將給定的singleton物件新增到此工廠的singleton快取中。
* 將bean載入到應用程式上下文中。
*/
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
/**
* 重新整理上下文
* 1、同步重新整理,對上下文的bean工廠包括子類的重新整理準備使用,初始化此上下文的訊息源,註冊攔截bean的處理器,檢查偵聽器bean並註冊它們,例項化所有剩餘的(非延遲-init)單例。
* 2、非同步開啟一個同步執行緒去時時監控容器是否被關閉,當關閉此應用程式上下文,銷燬其bean工廠中的所有bean。
* 。。。底層調refresh方法程式碼量較多
*/
refreshContext(context);
afterRefresh(context, applicationArguments);
// stopwatch 的作用就是記錄啟動消耗的時間,和開始啟動的時間等資訊記錄下來
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 釋出一個已啟動的事件
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// 釋出一個執行中的事件
listeners.running(context);
}
catch (Throwable ex) {
// 啟動異常,裡面會釋出一個失敗的事件
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
(3)、構建容器環境
在:run方法中的ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
是準備環境,裡面會載入配置檔案;
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
// 建立一個配置環境,根據前面定義的應用型別定義不同的環境
ConfigurableEnvironment environment = getOrCreateEnvironment();
// 將配置引數設定到配置環境中
configureEnvironment(environment, applicationArguments.getSourceArgs());
/**
* 釋出一個環境裝載成功的事件,並呼叫支援此事件的監聽器
* 這其中就有我們今天的主角:配置檔案載入監聽器(ConfigFileApplicationListener)
*/
listeners.environmentPrepared(environment);
// 將配置環境繫結到應用程式
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader())
.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
(4)、ConfigFileApplicationListener類介紹
sb就是通過ConfigFileApplicationListener 這個類來載入配置檔案的,這個類同樣是一個監聽器,我們來看看他的繼承類圖:
再讓我們來看看這個類具體都有哪些方法:
最後我們來看看這個類有哪些需要注意的欄位:
(5)、ConfigFileApplicationListener類載入配置檔案
我們從ConfigFileApplicationListener.onApplicationEvent開始,一直往下看方法鏈,發現最後是load方法去具體怎麼載入配置檔案的
啟用配置檔案與預設配置檔案的優先順序:
我們在使用中經常會根據不同的環境根據spring.profiles.active
屬性來定義不同的配置檔案:
- application-dev.properties
- application-test.properties
- application-prod.properties
但同時我們會建立一個預設的配置檔案:application.properties
,那自定義環境的配置檔案與預設的配置檔案的優先順序是哪個高呢?
看圖片我們可知他們載入的先後順序(注意:後載入會覆蓋前載入的檔案):
- application-xxx.properties
- application.properties
配置檔案路徑的優先順序:
我們從屬性:DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/
可以看出檔案路徑的先後順序(注意:後載入的會覆蓋先載入的):
- classpath:/
- classpath:/config/
- file:./
- file:./config/
配置檔案的優先順序:
我們從這個類中的欄位:propertySourceLoaders可以看出有兩個Loader,請各位看官看圖:
我們從上面兩張圖中可以看出,每個Loader會載入兩種字尾名的檔案,加起來就是4種,又因為是陣列型別,所以也會有先後順序,所以載入配置檔案的先後順序就是(後載入覆蓋先載入的):
- properties
- xml
- yml
- yaml
最後查詢的具體路徑:location + name + "-" + profile + "." + ext
這裡我們介紹了三種優先順序:
- active與預設優先順序
- 檔案路徑優先順序
- 檔案字尾優先順序
未完待續。。。
四、提問
springboot學習遺留問題,
1.active和預設的誰覆蓋誰
2.flter區別
3.多個配置檔案如何覆蓋