SpringBoot是如何載入配置檔案的?

架構文摘發表於2019-11-02

前言

本文針對版本2.2.0.RELEASE來分析SpringBoot的配置處理原始碼,通過檢視SpringBoot的原始碼來弄清楚一些常見的問題比如:

  1. SpringBoot從哪裡開始載入配置檔案?
  2. SpringBoot從哪些地方載入配置檔案?
  3. SpringBoot是如何支援yamlproperties型別的配置檔案?
  4. 如果要支援json配置應該如何做?
  5. SpringBoot的配置優先順序是怎麼樣的?
  6. placeholder是如何被解析的?

帶著我們的問題一起去看一下SpringBoot配置相關的原始碼,找出問題的答案。

SpringBoot從哪裡開始載入配置檔案?

SpringBoot載入配置檔案的入口是由ApplicationEnvironmentPreparedEvent事件進入的,SpringBoot會在SpringApplication的建構函式中通過spring.factories檔案獲取ApplicationListener的例項類:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
	...
	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    ...
}
複製程式碼

spring.factories中有一個ConfigFileApplicationListener類,它會監聽ApplicationEnvironmentPreparedEvent然後再載入配置檔案 :

# Application Listeners
org.springframework.context.ApplicationListener= org.springframework.boot.context.config.ConfigFileApplicationListener
...
複製程式碼

有了事件和事件處理的類後,再找出傳送事件的地方,就可以搞清楚SpringBoot是怎麼載入配置檔案的了,SpringBoot在啟動之前先初始化好SpringApplicationRunListeners這個類,它會實現SpringApplicationRunListener介面然後對事件進行轉發:

class SpringApplicationRunListeners {

	private final Log log;

	private final List<SpringApplicationRunListener> listeners;

	SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {
		this.log = log;
		this.listeners = new ArrayList<>(listeners);
	}
 
	void environmentPrepared(ConfigurableEnvironment environment) {
		for (SpringApplicationRunListener listener : this.listeners) {
			listener.environmentPrepared(environment);
		}
	}
	...
}
複製程式碼

獲取SpringApplicationRunListeners的程式碼如下:


private SpringApplicationRunListeners getRunListeners(String[] args) {
	Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
	return new SpringApplicationRunListeners(logger,
			getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

複製程式碼

同樣也會去載入spring.factories檔案,該檔案有一個EventPublishingRunListener類,該類的作用就是SpringBoot的事件轉換成ApplicationEvent傳送出去。

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
複製程式碼

小結

  • SpringBoot會將事件轉換成ApplicationEvent再分發
  • SpringBoot是通過監聽ApplicationEnvironmentPreparedEvent事件來載入配置檔案的
  • ConfigFileApplicationListener是處理配置檔案的主要類

SpringBoot從哪些地方載入配置檔案?

上面已經分析到ConfigFileApplicationListener是處理配置檔案的主要類,然後進一步的檢視SpringBoot是從哪些地址載入配置檔案,進入ConfigFileApplicationListener類後會有兩個預設的常量:

private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
private static final String DEFAULT_NAMES = "application";
複製程式碼

首先在沒有任何配置的情況下,會從DEFAULT_SEARCH_LOCATIONS常量列出來的位置中載入檔名為DEFAULT_NAMES(.properties或yml)的檔案,預設位置包括:

  • classpath根目錄(classpath:/)
  • classpath裡面的config檔案目錄(classpath:/config/)
  • 程式執行目錄(file:./)
  • 程式執行目錄下的config目錄(file:./config/)

上面說的是沒有額外配置的情況,SpringBoot足夠靈活可以指定配置檔案搜尋路徑、配置檔名,在ConfigFileApplicationListener類中有個getSearchLocations方法,它主要負責獲取配置搜尋目錄:

private Set<String> getSearchLocations() {
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
		return getSearchLocations(CONFIG_LOCATION_PROPERTY);
	}
	Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
	locations.addAll(
			asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
	return locations;
}
複製程式碼

它的操作步驟大致如下:

  1. 檢查是否有spring.config.location屬性,如果存在則直接使用它的值
  2. spring.config.additional-location屬性中獲取搜尋路徑
  3. 將預設搜尋路徑新增到搜尋集合

這裡就可以確定SpringBoot配置的搜尋路徑有兩種情況:如果配置了spring.config.location則直接使用,否則使用spring.config.additional-location的屬性值 + 預設搜尋路徑。

SpringBoot是如何支援yamlproperties型別的配置檔案?

SpringBoot的配置支援propertiesyaml檔案,SpringBoot是如何解析這兩種檔案的呢,繼續分析ConfigFileApplicationListener這個類,裡面有個子類叫Loader載入配置檔案主要的工作就是由這貨負責,但是直接讀取propertiesyaml並轉換成PropertySource還是由裡面的PropertySourceLoader負責:

Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
	...
	this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
			getClass().getClassLoader());
}
複製程式碼

構造Loader物件的時候就會先載入PropertySourceLoader,載入方式還是從spring.factories中讀取:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
複製程式碼

其中配置了兩個PropertySourceLoader的實現類:

  • PropertiesPropertySourceLoader
  • YamlPropertySourceLoader

看名字就知道是分別負責propertiesyaml的啦。

如果要支援json配置應該如何做?

如果不喜歡propertiesyaml這兩種格式,想要定義json做為配置文字格式可以直接定義json型別的PropertySourceLoader:

public class JSONPropertySourceLoader implements PropertySourceLoader {

    @Override
    public String[] getFileExtensions() {
        return new String[] {"json"};
    }
    
    @Override
    public List<PropertySource<?>> load(String name, Resource resource) throws IOException {

        if(resource == null || !resource.exists()){
            return Collections.emptyList();
        }

        Map<String, Object> configs = JSON.parseObject(resource.getInputStream(), Map.class);

        return Collections.singletonList(
                new MapPropertySource(name, configs)
        );
    }
}
複製程式碼

然後在resources目錄裡面建立個META-INF,再新增個spring.factories裡面的內容如下:

org.springframework.boot.env.PropertySourceLoader=\
com.csbaic.arch.spring.env.loader.JSONPropertySourceLoader
複製程式碼

最後在resources目錄裡面建個application.json的配置檔案 :

{
  "spring.application.name": "JSONConfig"
}
複製程式碼

正常啟動SpringBoot獲取spring.applicaiton.name的配置的值就是JSONConfig

2019-11-02 14:50:17.730  INFO 55275 --- [           main] c.c.a.spring.env.SpringEnvApplication    : JSONConfig
複製程式碼

SpringBoot的配置優先順序是怎麼樣的?

SpringBoot中有個PropertySource介面,專門用來儲存屬性常見的實現類有:

  • CommandLinePropertySource
  • MapPropertySource
  • SystemEnvironmentPropertySource
  • ....

另外為了集中管理PropertySource還抽象出一個PropertySources介面,PropertySources就一個實現類叫:MutablePropertySources,它將所有的PropertySource都放置在一個名叫propertySourceList集合中,同時提供一些修改操作方法:

public void addFirst(PropertySource<?> propertySource) {}
public void addLast(PropertySource<?> propertySource) {}
public void addBefore(String relativePropertySourceName, PropertySource<?> propertySource) {}
public void addAfter(String relativePropertySourceName, PropertySource<?> propertySource) {}
public int precedenceOf(PropertySource<?> propertySource) {	}
public PropertySource<?> remove(String name) {}
public void replace(String name, PropertySource<?> propertySource) {}
複製程式碼

所有的PropertySource都儲存在propertySourceList中,越小的索引優先順序越高,所以如果想要覆蓋屬性只要保證優化級夠高就行。

placeholder是如何被解析的?

繼續分析ConfigFileApplicationListenerLoader子類,在構造時還會建立一個PropertySourcesPlaceholdersResolver,placeholder的解析都由它來完成:

Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {

	this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
}
複製程式碼

分析PropertySourcesPlaceholdersResolver發現,真正完成解析是由PropertyPlaceholderHelper完成,PropertySourcesPlaceholdersResolver 在構造的時候就會建立一個PropertyPlaceholderHelper

public PropertySourcesPlaceholdersResolver(Iterable<PropertySource<?>> sources, PropertyPlaceholderHelper helper) {
	this.sources = sources;
	this.helper = (helper != null) ? helper : new PropertyPlaceholderHelper(SystemPropertyUtils.PLACEHOLDER_PREFIX,
			SystemPropertyUtils.PLACEHOLDER_SUFFIX, SystemPropertyUtils.VALUE_SEPARATOR, true);
}
複製程式碼

PropertySourcesPlaceholdersResolver 在建立 PropertyPlaceholderHelper 的時候會傳遞三個引數:字首、字尾、預設值分割符,分別由以下三個常量表示:

public static final String PLACEHOLDER_PREFIX = "${";
public static final String PLACEHOLDER_SUFFIX = "}";
public static final String VALUE_SEPARATOR = ":";
複製程式碼

這樣 PropertyPlaceholderHelper 在解析placeholder時就能知道以什麼格式來解析比如:${spring.application.name}這個placeholder就會被解析成屬性值。

總結

SpringBoot的配置非常靈活配置可以來自檔案、環境變數、JVM系統屬性、配置中心等等,SpringBoot通過 PropertySourcePropertySources實現屬性優先順序、CRUD的統一管理,為開發者提供統一的配置抽象。



SpringBoot是如何載入配置檔案的?
《架構文摘》每天一篇架構領域重磅好文,涉及一線網際網路公司應用架構(高可用、高性> 能、高穩定)、大資料、機器學習等各個熱門領域。

相關文章