前言
最近在學習Spring Boot相關的課程,過程中以筆記的形式記錄下來,方便以后回憶,同時也在這里和大家探討探討,文章中有漏的或者有補充的、錯誤的都希望大家能夠及時提出來,本人在此先謝謝了!
開始之前呢,希望大家帶着幾個問題去學習:
1、Spring Boot 外部化配置是什么?
2、整體流程或結構是怎樣的?
3、核心部分是什么?
4、怎么實現的?
這是對自我的提問,我認為帶着問題去學習,是一種更好的學習方式,有利於加深理解。好了,接下來進入主題。
1、起源
這篇文章我們就來討論 Spring Boot
的外部化配置功能,該功能主要是通過外部的配置資源實現與代碼的相互配合,來避免硬編碼,提供應用數據或行為變化的靈活性。相信小伙伴們在日常工作中都有使用過,如在 properties
或者 YAML
文件中定義好key value格式的數據后,就可在程序中通過 @Value
注解獲取該value值。還可以定義一些同外部組件約定好的key,如以 spring
或 redis
等組件名為前綴的key,之后相應的組件就可讀取到該value值進行工作。當然,這只是外部化配置的一小部分內容,接下來進行詳細討論。
注:本篇文章所用到的
Spring Boot
版本是2.1.6.BUILD-SNAPSHOT
2、外部化配置的資源類型
先來看看外部化配置的幾種資源類型,除了 properties
和 YAML
外,還有環境變量、系統屬性、啟動參數等。所有的資源類型將近二十種,這里只介紹我們比較熟悉的:
- properties :這個應該都知道,就是在以
.properties
為后綴的文件中定義key value格式數據。 - YAML:文件格式是以
.yml
為后綴,文件中的數據也是key value格式,如下:
user:
name: loong
age: 10
這里的key就是 user.name
、 user.age
。
3. 環境變量:這是通過 System.getenv()
方式獲取的默認配置,也是key value格式,下面列出部分配置,其它的還請自行了解,如下:
名稱 | Key |
---|---|
Java安裝目錄 | JAVA_HOME |
classpath環境變量 | CLASSPATH |
用戶臨時文件目錄 | TEMP |
計算機名 | COMPUTERNAME |
用戶名 | USERNAME |
- 系統屬性:這是通過
System.getProperties()
方式獲取的默認配置,也是key value格式,下面列出部分配置,其它的還請自行了解,如下:
名稱 | Key |
---|---|
運行時環境版本 | java.version Java |
Java安裝目錄 | java.home |
要使用的 JIT編譯器的名稱 | java.compiler |
操作系統的架構 | os.arch |
操作系統的版本 | os.version |
- 啟動參數:這個在 《Spring Boot SpringApplication 啟動類(二)》這篇文章中討論過。一種是在
jar
包運行時行時傳遞的參數,如:java -jar xxx.jar name=張三 pwa=123
,還有一種是在 IDEA 的Program arguments
中輸入數據:
可以看到,外部化配置中的數據都是key value 格式。這里還要注意它們的加載順序,當key相同時,會出現覆蓋的情況。
3、外部化配置的核心
接下來,我們的重心來圍繞 properties
和 YAML
配置文件,這兩者也是我們日常工作中常用的。首先來看取值方式,在 Spring
時代有 Environment
、 @Value
、 XML
三種方式,在 Spring Boot
時代則是 @ConfigurationProperties
方式。其中,涉及到了一個核心類,它就是 Environment
,該對象不僅可以獲取所有的外部化配置數據,就連另外幾種取值方式的數據來源也是從該類中獲取。這里,主要對 Environment
和 @ConfigurationProperties
進行詳細討論,筆者認為 Environment
和 @ConfigurationProperties
才是 Spring Boot
外部化配置的核心所在。
3.1 Environment
該類在 《Spring Boot SpringApplication啟動類(二)》 的 2.3 章節有簡要說明過,這里我們展開詳細討論。首先回顧一下代碼:
public class SpringApplication {
...
public ConfigurableApplicationContext run(String... args) {
...
try {
...
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
...
}
...
}
在 SpringApplication
運行階段的 run
方法中通過 prepareEnvironment
方法了創建 ConfigurableEnvironment
的實現類對象,ConfigurableEnvironment
是一個接口,且繼承了 Environment
。進入創建方法:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
首先看第一行,通過 getOrCreateEnvironment
方法創建 ConfigurableEnvironment
對象:
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}
在 《Spring Boot SpringApplication 啟動類(二)》 的 2.3 章節說過, webApplicationType
存儲的是應用的類型,有 Reactive
、Servlet
等,是在 SpringApplication
准備階段推導出來的,而本項目推導出來是 Servlet
類型,所以實例化的是 StandardServletEnvironment
對象:
public class StandardServletEnvironment extends StandardEnvironment implements ConfigurableWebEnvironment {
public static final String SERVLET_CONTEXT_PROPERTY_SOURCE_NAME = "servletContextInitParams";
public static final String SERVLET_CONFIG_PROPERTY_SOURCE_NAME = "servletConfigInitParams";
public static final String JNDI_PROPERTY_SOURCE_NAME = "jndiProperties";
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new StubPropertySource(SERVLET_CONFIG_PROPERTY_SOURCE_NAME));
propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_PROPERTY_SOURCE_NAME));
if (JndiLocatorDelegate.isDefaultJndiEnvironmentAvailable()) {
propertySources.addLast(new JndiPropertySource(JNDI_PROPERTY_SOURCE_NAME));
}
super.customizePropertySources(propertySources);
}
...
}
而該類又繼承了 StandardEnvironment
類。且重寫了 customizePropertySources
方法,並調用了父類的 customizePropertySources
方法。我們繼續往下深入:
public class StandardEnvironment extends AbstractEnvironment {
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment";
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties";
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(
new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(
new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
}
繼續看它的 AbstractEnvironment
父抽象類:
public abstract class AbstractEnvironment implements ConfigurableEnvironment {
...
private final MutablePropertySources propertySources = new MutablePropertySources();
...
public AbstractEnvironment() {
customizePropertySources(this.propertySources);
}
...
}
可以看到,最終會有一個 AbstractEnvironment
抽象類。在 StandardServletEnvironment
初始化時,會調用 AbstractEnvironment
的構造方法,里面調用了子類重寫的 customizePropertySources
方法,且入參是 MutablePropertySources
對象,該對象是 Environment
的一個屬性,是底層真正存儲外部化配置的。之后, StandardServletEnvironment
和 StandardEnvironment
的 customizePropertySources
方法相繼執行,主要是往 MutablePropertySources
對象中添加外部化配置。其中我們前面所說的環境變量和系統屬性是在 StandardEnvironment
重寫的方法中進行加載。
我們回到外面的 prepareEnvironment
方法,繼續往下走。接着執行的是 configureEnvironment
方法,該方法主要是把啟動參數加入到 MutablePropertySources
中。之后,我們斷點看看有多少種外部化配置:
有五種,且真正存儲數據的是 MutablePropertySources
中的 PropertySource
實現類集合。
這里簡要介紹一下 PropertySource
,我們將其稱之為配置源,官方定義它是外部化配置的API描述方式,是外部化配置的一個媒介。 用我們的話來說,它是一個抽象類,提供了統一存儲外部化配置數據的功能,而每種外部化配置有具體的實現類,主要提供不同的基礎操作,如 get
、contains
等 。我們來看看 PropertySource
對象的數據格式,一般包含:
name : 外部化配置的名稱
source : 存儲配置中的數據,底層一般數據格式都是key value
我們繼續往下走,接着調用了 SpringApplicationRunListeners
的 environmentPrepared
方法。在上篇文章
《Spring Boot SpringApplication 啟動類(二)》 的 2.1 小節講過,當 Spring Boot
執行到某一階段時,會通過 Spring
的 SimpleApplicationEventMulticaster
事件廣播器進行事件廣播,之后 ,相應監聽器就會監聽到該事件,執行調監聽器的 onApplicationEvent
方法。這里表示 Spring Boot
到了 ConfigurableEnvironment
構建完成時階段。我們進入該方法:
class SpringApplicationRunListeners {
...
private final List<SpringApplicationRunListener> listeners;
public void environmentPrepared(ConfigurableEnvironment environment) {
for (SpringApplicationRunListener listener : this.listeners) {
listener.environmentPrepared(environment);
}
}
...
}
真正調用的是 SpringApplicationRunListener
集合中的 environmentPrepared
方法。 SpringApplicationRunListener
是一個接口,它具有唯一實現類 EventPublishingRunListener
:
public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
...
private final SimpleApplicationEventMulticaster initialMulticaster;
@Override
public void environmentPrepared(ConfigurableEnvironment environment) {
this.initialMulticaster
.multicastEvent(new ApplicationEnvironmentPreparedEvent(this.application, this.args, environment));
}
...
}
可以看到,最終通過 SimpleApplicationEventMulticaster
的 multicastEvent
方法發布 ApplicationEnvironmentPreparedEvent
事件。上面說過, Spring Boot
監聽器會監聽到該事件,其中一個名為 ConfigFileApplicationListener
的監聽器,監聽到該事件后會進行加載 application
和 YAML
配置文件的操作,接下來,我們具體的來看一看該類實現。
3.1.1、ConfigFileApplicationListener
我們直接進入該類:
public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
...
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
public static final String CONFIG_NAME_PROPERTY = "spring.config.name";
public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";
private static final String DEFAULT_NAMES = "application";
@Override
public void onApplicationEvent(ApplicationEvent event) {
// 1、通過 instanceof 判斷事件的類型,如果是 ApplicationEnvironmentPreparedEvent 事件,則執行 onApplicationEnvironmentPreparedEvent 方法
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
...
}
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
// 2、調用 loadPostProcessors 方法,返回 Environment 的后置處理器集合,我們跳到 2.1 查看方法實現
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
// 2.2、把自己也加入該集合
postProcessors.add(this);
AnnotationAwareOrderComparator.sort(postProcessors);
// 2.3、遍歷 EnvironmentPostProcessor 集合,執行它們的 postProcessEnvironment 方法,我們跳到 3 查看當前類的該方法實現
for (EnvironmentPostProcessor postProcessor : postProcessors) {
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}
// 2.1 是我們比較熟悉的 loadFactories 方法,在 Spring Boot 自動裝配(二) 的 2.1.2 小節講過,loadFactories 方法是從 spring.factories 文件中加載 key 為 EnvironmentPostProcessor 的實現類集合
List<EnvironmentPostProcessor> loadPostProcessors() {
return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader());
}
// 3、 執行到該方法時,會調用 addPropertySources 方法,入參是上文加載 ConfigurableEnvironment 對象
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
addPropertySources(environment, application.getResourceLoader());
}
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
RandomValuePropertySource.addToEnvironment(environment);
// 4、 我們主要關注這里,通過 Loader 的構造方法創建該對象,並調用它的 load 方法
new Loader(environment, resourceLoader).load();
}
private class Loader {
private final ConfigurableEnvironment environment;
private final List<PropertySourceLoader> propertySourceLoaders;
// 4.1、 構造方法中會初始化一些屬性
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
...
this.environment = environment;
// 又是我們比較熟悉的 loadFactories 方法,在 Spring Boot 自動裝配(二) 的 2.1.2 小節講過,loadFactories 方法是從 spring.factories 文件中加載 key 為 PropertySourceLoader 的實現類集合。這里加載的是 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader 兩個實現類,看類名可初步斷定是處理 properties 和 YAML 文件的
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,getClass().getClassLoader());
}
public void load() {
...
// 5、這里會繼續調用它重載的 load 方法
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
...
// 9、這是最后一步,將當前類中的 MutablePropertySources 中的 PropertySource 對象,全部塞到 ConfigurableEnvironment 的 MutablePropertySources 對象中。我們跳到 9.1 進行查看
addLoadedPropertySources();
}
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
// 5.1、首先執行 getSearchLocations 方法,看方法名大致能猜出是獲取搜索路徑的,我們跳到 5.1.1 查看該方法的實現
getSearchLocations().forEach((location) -> {
// 5.2、開始遍歷該集合,先判斷該路徑是否是以反斜杠結尾,是的話則該路徑為文件夾;不是的話,則該路徑為文件的完整路徑,類似於 classPath:/application.properties
boolean isFolder = location.endsWith("/");
// 5.3、 如果是文件夾路徑,則通過 getSearchNames 獲取文件的名稱,不是則返回空集合,我們跳到 5.3.1 查看 getSearchNames 方法
Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
// 5.4、再調用 load 的重載方法,這里,location 是路徑名,name是文件名,我們跳到 6 進行查看
names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
});
}
// 5.1.1、這個方法就是獲取加載 application 和 YAML 文件路徑的
private Set<String> getSearchLocations() {
// 可以看到 CONFIG_LOCATION_PROPERTY 的值為 spring.config.location,也就是說,先判斷我們有沒有手動設置搜索路徑,有的話直接返回該路徑。該值一般通過啟動參數的方式設置
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
return getSearchLocations(CONFIG_LOCATION_PROPERTY);
}
// 該 CONFIG_ADDITIONAL_LOCATION_PROPERTY 變量的值為 spring.config.additional-location,這也是用於手動設置搜索路徑,不過和上面不同的是,不會覆蓋 接下來默認的搜索路徑
Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY);
// 這里就是獲取默認的搜索路徑,通過 DEFAULT_SEARCH_LOCATIONS 變量的值 classpath:/,classpath:/config/,file:./,file:./config/,將該值用逗號分隔,加入集合並返回。到這一步,我們至少獲取到了4個加載 application 和 YAML 文件的路徑
locations.addAll(asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS));
return locations;
}
// 5.3.1
private Set<String> getSearchNames() {
// CONFIG_LOCATION_PROPERTY 變量值為 spring.config.name ,同樣先判斷有沒有手動設置文件名稱,有的話,直接返回
if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
return asResolvedSet(property, null);
}
// 如果沒有,則通過 DEFAULT_NAMES 變量值返回默認的文件名,變量值為 application
return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}
// 6、
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
// 6.1、 上面 5.2 說過 name 為空時,表示 location 是完整的文件路徑。之后進入這個 if
if (!StringUtils.hasText(name)) {
// 6.1.1、propertySourceLoaders 屬性是在 4.1 處被初始化的,存儲的是 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader 兩個類。這里對這兩個類進行遍歷
for (PropertySourceLoader loader : this.propertySourceLoaders) {
// 我們跳到 6.1.2 查看 canLoadFileExtension 方法實現,入參 location 是文件的完整路徑
if (canLoadFileExtension(loader, location)) {
// 這里又是一個 load 重載方法,我們跳到 7 進行查看
load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
return;
}
}
}
Set<String> processed = new HashSet<>();
for (PropertySourceLoader loader : this.propertySourceLoaders) {
// 6.2 這里和 6.1.3 類似,獲取文件擴展名
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
// 進入 6.3、查看該方法實現。關注重點的兩個參數:一個是路徑名 + 文件名,還有一個 “.” +文件擴展名
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
consumer);
}
}
}
}
// 6.1.2、 該方法作用是 判斷 name 完整路徑名是否以指定的文件擴展名結尾
private boolean canLoadFileExtension(PropertySourceLoader loader, String name) {
// 6.1.3、調用 PropertySourceLoader 的 getFileExtensions 方法。當你的實現類是 PropertiesPropertySourceLoader 時,該方法返回 properties、xml;如果是 YamlPropertySourceLoader 則返回 yml、yaml。從這里可以看出,能被處理的文件格式有這四種
return Arrays.stream(loader.getFileExtensions())
.anyMatch((fileExtension) -> StringUtils.endsWithIgnoreCase(name, fileExtension));
}
// 6.3 到了這里,prefix 和 fileExtension 都是進行拼接好的值,如 prefix = classpath:/applicarion,fileExtension = .properties
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
...
// 這里同樣調用節點 7 的重載方法,通過 prefix + fileExtension 形成完整的文件路徑名,通過入參進行傳遞。如 classpath:/applicarion.properties
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}
// 7、
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
DocumentConsumer consumer) {
try {
// 這里調用 ResourceLoader 的 getResource 方法,通過 location 文件路徑,讀取獲取該文件資源,之后就好辦了
Resource resource = this.resourceLoader.getResource(location);
...
// 具體解析在過程 loadDocuments 中,這里就不繼續跟蹤了,大致是以流的方式解析文件。解析之后會生成一個 PropertySource 對象,該對象在上面說過,表示一個外部化配置源對象,存儲配置中的數據。之后,會將該對象封裝到 Document 中
List<Document> documents = loadDocuments(loader, name, resource);
...
if (!loaded.isEmpty()) {
// 遍歷 documents 集合,當執行 consumer.accept 時會進入 addToLoaded 方法,這是 Java8 的寫法。consumer 對象參數來自節點 5 。我們跳到 8 查看 addToLoaded 實現
loaded.forEach((document) -> consumer.accept(profile, document));
if (this.logger.isDebugEnabled()) {
StringBuilder description = getDescription("Loaded config file ", location, resource, profile);
this.logger.debug(description);
}
}
}
catch (Exception ex) {
throw new IllegalStateException("Failed to load property " + "source from location '" + location + "'",
ex);
}
}
// 8、BiConsumer 是 JAVA8 的函數接口,表示定義一個帶有兩個參數且不返回結果的操作,通過節點 5 我們知道,這個操作是 MutablePropertySources::addFirst 。
private DocumentConsumer addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
boolean checkForExisting) {
return (profile, document) -> {
if (checkForExisting) {
for (MutablePropertySources merged : this.loaded.values()) {
if (merged.contains(document.getPropertySource().getName())) {
return;
}
}
}
MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
(k) -> new MutablePropertySources());
// 當調用 BiConsumer 的 accept 方法時,定義的操作會執行,兩個入參分別是 MutablePropertySources 對象和配置文件源對象 PropertySource。該操作會調用 MutablePropertySources 的 addFirst 方法把該配置文件源對象添加至其中。最后我們去看看前面 load 方法中的最后一步 9
addMethod.accept(merged, document.getPropertySource());
};
}
// 9.1
private void addLoadedPropertySources() {
// 獲取當前上下文環境中的 MutablePropertySources 對象
MutablePropertySources destination = this.environment.getPropertySources();
// 獲取當前類中的 MutablePropertySources 集合
List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
Collections.reverse(loaded);
String lastAdded = null;
Set<String> added = new HashSet<>();
// 遍歷 loaded 集合及其中所有的 PropertySource ,也就是 application 或 YAML 配置文件源對象
for (MutablePropertySources sources : loaded) {
for (PropertySource<?> source : sources) {
if (added.add(source.getName())) {
// 我們進入 9.2 查看該方法,主要參數是上下文環境中的 MutablePropertySources 對象和配置文件源對象
addLoadedPropertySource(destination, lastAdded, source);
lastAdded = source.getName();
}
}
}
}
// 9.2
private void addLoadedPropertySource(Mutab lePropertySources destination, String lastAdded,
PropertySource<?> source) {
if (lastAdded == null) {
if (destination.contains(DEFAULT_PROPERTIES)) {
destination.addBefore(DEFAULT_PROPERTIES, source);
}
else {
destination.addLast(source);
}
}
else {
// 最后通過將 source 添加到 environment 中的 MutablePropertySources 對象中。
destination.addAfter(lastAdded, source);
}
}
// 至此,properties 和 YAML 配置文件就被加載到了上下文環境共享的 Environment 中,之后如 @Value 等獲取值都是從該對象中獲取
}
}
可以看到,ConfigFileApplicationListener
主要功能就是將 properties
和 YAML
文件加載到 Environment
中。另外還存在一個 @PropertySource
注解,也是加載指定的配置文件到 Environment
中。
3.1.2、關聯 SpringConfigurationPropertySources
我們回到最外面的 prepareEnvironment
方法,來看看執行完監聽方法時 ConfigurableEnvironment
中加載了多少種外部化配置:
有七種,包括新增的 properties
配置文件。
之后還有一個操作,通過 ConfigurationPropertySources.attach
關聯 SpringConfigurationPropertySources
類,該類主要是做一個適配器的工作,將 MutablePropertySources
轉換為 ConfigurationPropertySource
,下一小節將會用到對象。我們進入 attach 方法查看:
public final class ConfigurationPropertySources {
private static final String ATTACHED_PROPERTY_SOURCE_NAME = "configurationProperties";
public static void attach(Environment environment) {
Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
// 獲取 ConfigurableEnvironment 中的 MutablePropertySources
MutablePropertySources sources = ((ConfigurableEnvironment) environment).getPropertySources();
// 獲取名為 configurationProperties 的外部化配置源對象
PropertySource<?> attached = sources.get(ATTACHED_PROPERTY_SOURCE_NAME);
// 如果存在,則把該對象移除
if (attached != null && attached.getSource() != sources) {
sources.remove(ATTACHED_PROPERTY_SOURCE_NAME);
attached = null;
}
// 不存在,則添加一個配置源對象,具體對象類型為 ConfigurationPropertySourcesPropertySource,源對象中的數據為 SpringConfigurationPropertySources
if (attached == null) {
sources.addFirst(new ConfigurationPropertySourcesPropertySource(ATTACHED_PROPERTY_SOURCE_NAME,
new SpringConfigurationPropertySources(sources)));
}
}
}
到這里,ConfigurableEnvironment
又新增了一個 ConfigurationPropertySourcesPropertySource
類型的配置源對象。我們主要來關注 SpringConfigurationPropertySources
對象,可以看到,這里是通過它的帶參構造器創建該對象,參數 sources
是從 ConfigurableEnvironment
中獲取的 MutablePropertySources
對象。我們進入 SpringConfigurationPropertySources
類中查看:
class SpringConfigurationPropertySources implements Iterable<ConfigurationPropertySource> {
...
private final Iterable<PropertySource<?>> sources;
SpringConfigurationPropertySources(Iterable<PropertySource<?>> sources) {
Assert.notNull(sources, "Sources must not be null");
this.sources = sources;
}
...
}
可以看到,外部 ConfigurableEnvironment
的 MutablePropertySources
關聯到了該類中的 Iterable
(繼承關系) 對象,這就是在進行適配工作。
至此, Environment
的創建過程及加載外部化配置的過程就到這里結束,我們簡要回顧一下該流程:
- 首先
Environment
是一個較為特殊的類,術語稱之為應用運行時的環境。它存儲了所有的外部化配置,可以通過它獲取任意配置數據,並且@Value
、@ConfigurationProperties
等其它獲取配置數據的方式都依賴於該類。 - 通過判斷應用的類型,來創建不同環境的
Environment
,有Servlet
、Reactive
、非 Web 類型。 - 之后會相繼添加外部化配置到該類中,每種外部化配置都對應了一個
PropertySource
配置源對象。 - 重點介紹了加載
properties
和YAML
的方式。主要是通過回調Spring Boot
的監聽器ConfigFileApplicationListener
進行處理。
加載完之后,就可以在應用中通過 @Autowired
注入該對象,獲取任意外部化配置屬性。
接下來要討論的是在 Spring Boot
時代是如何通過 @ConfigurationProperties
來獲取配置屬性的。因篇幅過長, @ConfigurationProperties
內容另起一章。