SpringBoot啟動如何載入application.yml配置檔案

豬哥66發表於2018-11-23

一、前言

在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自動載入配置檔案的機制!

看完本篇文章你將瞭解到:

  1. springboot什麼時候載入配置檔案
  2. springboot通過哪個類載入配置檔案
  3. springboot自動載入配置檔案流程
  4. 啟用檔案優先順序
  5. 檔案載入路徑優先順序
  6. 檔案字尾優先順序

二、提出猜想

我們知道在使用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

這裡我們介紹了三種優先順序:

  1. active與預設優先順序
  2. 檔案路徑優先順序
  3. 檔案字尾優先順序
    未完待續。。。

四、提問

springboot學習遺留問題,
1.active和預設的誰覆蓋誰
2.flter區別
3.多個配置檔案如何覆蓋

相關文章