參考 知識星球 中 芋道源碼 星球的源碼解析,一個活躍度非常高的 Java 技術社群,感興趣的小伙伴可以加入 芋道源碼 星球,一起學習😄
該系列文章是筆者在學習 Spring Boot 過程中總結下來的,里面涉及到相關源碼,可能對讀者不太友好,請結合我的源碼注釋 Spring Boot 源碼分析 GitHub 地址 進行閱讀
Spring Boot 版本:2.2.x
最好對 Spring 源碼有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 - 文章導讀》 系列文章
如果該篇內容對您有幫助,麻煩點擊一下“推薦”,也可以關注博主,感激不盡~
該系列其他文章請查看:《精盡 Spring Boot 源碼分析 - 文章導讀》
概述
在我們的 Spring Boot 應用中,可以很方便的在 application.yml 或 application.properties 文件中添加需要的配置信息,並應用於當前應用。那么,對於 Spring Boot 是如何加載配置文件,如何按需使指定環境的配置生效的呢?接下來,我們一起來看看 Spring Boot 是如何加載配置文件的。
提示:Spring Boot 加載配置文件的過程有點繞,本篇文章有點長,可選擇性的跳過 Loader 這一小節
回顧
回到前面的 《SpringApplication 啟動類的啟動過程》 這篇文章,Spring Boot 啟動應用的入口和主流程都是在 SpringApplication#run(String.. args) 方法中。
在這篇文章的 6. prepareEnvironment 方法 小節中可以講到,會對所有的 SpringApplicationRunListener 廣播 應用環境已准備好 的事件,如下:
// SpringApplicationRunListeners.java
void environmentPrepared(ConfigurableEnvironment environment) {
// 只有一個 EventPublishingRunListener 對象
for (SpringApplicationRunListener listener : this.listeners) {
listener.environmentPrepared(environment);
}
}
只有一個 EventPublishingRunListener 事件發布器,里面有一個事件廣播器,封裝了幾個 ApplicationListener 事件監聽器,如下:
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
其中有一個 ConfigFileApplicationListener 對象,監聽到上面這個事件,會去解析 application.yml 等應用配置文件的配置信息
在 Spring Cloud 還會配置一個 BootstrapApplicationListener 對象,監聽到上面的這個事件會創建一個 ApplicationContext 作為當前 Spring 應用上下文的父容器,同時會讀取 bootstrap.yml 文件的信息
ConfigFileApplicationListener
org.springframework.boot.context.config.ConfigFileApplicationListener,Spring Boot 的事件監聽器,主要用於加載配置文件到 Spring 應用中
相關屬性
public class ConfigFileApplicationListener implements EnvironmentPostProcessor, SmartApplicationListener, Ordered {
/** 默認值的 PropertySource 在 Environment 中的 key */
private static final String DEFAULT_PROPERTIES = "defaultProperties";
// Note the order is from least to most specific (last one wins)
/** 支持的配置文件的路徑 */
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,classpath:/config/,file:./,file:./config/";
/** 配置文件名稱(不包含后綴) */
private static final String DEFAULT_NAMES = "application";
private static final Set<String> NO_SEARCH_NAMES = Collections.singleton(null);
private static final Bindable<String[]> STRING_ARRAY = Bindable.of(String[].class);
private static final Bindable<List<String>> STRING_LIST = Bindable.listOf(String.class);
/** 需要過濾的配置項 */
private static final Set<String> LOAD_FILTERED_PROPERTY;
static {
Set<String> filteredProperties = new HashSet<>();
filteredProperties.add("spring.profiles.active");
filteredProperties.add("spring.profiles.include");
LOAD_FILTERED_PROPERTY = Collections.unmodifiableSet(filteredProperties);
}
/**
* The "active profiles" property name.
* 可通過該屬性指定配置需要激活的環境配置
*/
public static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";
/**
* The "includes profiles" property name.
*/
public static final String INCLUDE_PROFILES_PROPERTY = "spring.profiles.include";
/**
* The "config name" property name.
* 可通過該屬性指定配置文件的名稱
*/
public static final String CONFIG_NAME_PROPERTY = "spring.config.name";
/**
* The "config location" property name.
*/
public static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
/**
* The "config additional location" property name.
*/
public static final String CONFIG_ADDITIONAL_LOCATION_PROPERTY = "spring.config.additional-location";
/**
* The default order for the processor.
*/
public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
private final DeferredLog logger = new DeferredLog();
private String searchLocations;
private String names;
private int order = DEFAULT_ORDER;
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return ApplicationEnvironmentPreparedEvent.class.isAssignableFrom(eventType)
|| ApplicationPreparedEvent.class.isAssignableFrom(eventType);
}
}
屬性不多,幾個關鍵的屬性都有注釋,同時支持處理的事件有 ApplicationEnvironmentPreparedEvent 和 ApplicationPreparedEvent
我們看到它實現了 EnvironmentPostProcessor 這個接口,用於對 Environment 進行后置處理,在刷新 Spring 應用上下文之前
1. onApplicationEvent 方法
onApplicationEvent(ApplicationEvent) 方法,ApplicationListener 處理事件的方法,如下:
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent(event);
}
}
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
// <1> 通過類加載器從 `META-INF/spring.factories` 文件中獲取 EnvironmentPostProcessor 類型的類名稱,並進行實例化
List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
// <2> 當前對象也是 EnvironmentPostProcessor 實現類,添加進去
postProcessors.add(this);
// <3> 將這些 EnvironmentPostProcessor 進行排序
AnnotationAwareOrderComparator.sort(postProcessors);
// <4> 遍歷這些 EnvironmentPostProcessor 依次對 Environment 進行處理
for (EnvironmentPostProcessor postProcessor : postProcessors) {
// <4.1> 依次對當前 Environment 進行處理,上面第 `2` 步添加了當前對象,我們直接看到當前類的這個方法
postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
}
}
我們在前面 回顧 中講到,會廣播一個 應用環境已准備好 的事件,也就是 ApplicationEnvironmentPreparedEvent 事件
處理該事件的過程如下:
-
通過類加載器從
META-INF/spring.factories文件中獲取EnvironmentPostProcessor類型的類名稱,並進行實例化List<EnvironmentPostProcessor> loadPostProcessors() { return SpringFactoriesLoader.loadFactories(EnvironmentPostProcessor.class, getClass().getClassLoader()); } -
當前對象也是
EnvironmentPostProcessor實現類,添加進去 -
將這些
EnvironmentPostProcessor進行排序 -
遍歷這些
EnvironmentPostProcessor依次對 Environment 進行處理- 依次對當前 Environment 進行處理,上面第
2步添加了當前對象,我們直接看到當前類的這個方法
- 依次對當前 Environment 進行處理,上面第
2. postProcessEnvironment 方法
postProcessEnvironment(ConfigurableEnvironment, SpringApplication) 方法,實現 EnvironmentPostProcessor 接口的方法,對 Environment 進行后置處理
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
// 為 Spring 應用的 Environment 環境對象添加屬性(包括 `application.yml`)
addPropertySources(environment, application.getResourceLoader());
}
直接調用 addPropertySources(..) 方法,為當前 Spring 應用的 Environment 環境對象添加屬性(包括 application.yml 配置文件的解析)
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
// <1> 往 Spring 應用的 Environment 環境對象添加隨機值的 RandomValuePropertySource 屬性源
// 這樣就可直接通過 `@Value(random.uuid)` 隨機獲取一個 UUID
RandomValuePropertySource.addToEnvironment(environment);
// <2> 創建一個 Loader 對象,設置占位符處理器,資源加載器,PropertySourceLoader 配置文件加載器
// <3> 加載配置信息,並放入 Environment 環境對象中
// 整個處理過程有點繞,嵌套有點深,你可以理解為會將你的 Spring Boot 或者 Spring Cloud 的配置文件加載到 Environment 中,並激活對應的環境
new Loader(environment, resourceLoader).load();
}
過程如下:
-
往 Spring 應用的 Environment 環境對象添加隨機值的
RandomValuePropertySource屬性源,這樣就可直接通過@Value(random.uuid)隨機獲取一個 UUID// RandomValuePropertySource.java public static void addToEnvironment(ConfigurableEnvironment environment) { environment.getPropertySources().addAfter(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, new RandomValuePropertySource(RANDOM_PROPERTY_SOURCE_NAME)); logger.trace("RandomValuePropertySource add to Environment"); }邏輯很簡單,感興趣的可以去看看
-
創建一個 Loader 對象,設置占位符處理器,資源加載器,PropertySourceLoader 配置文件加載器
-
調用這個 Loader 的
load()方法,加載配置信息,並放入 Environment 環境對象中
加載配置信息的過程有點繞,嵌套有點深,你可以先理解為,將你的 Spring Boot 或者 Spring Cloud 的配置文件加載到 Environment 中,並激活對應的環境
Loader
org.springframework.boot.context.config.ConfigFileApplicationListener.Loader,私有內部類,配置文件的加載器
構造方法
private class Loader {
/** 環境對象 */
private final ConfigurableEnvironment environment;
/** 占位符處理器 */
private final PropertySourcesPlaceholdersResolver placeholdersResolver;
/** 資源加載器 */
private final ResourceLoader resourceLoader;
/** 屬性的資源加載器 */
private final List<PropertySourceLoader> propertySourceLoaders;
/** 待加載的 Profile 隊列 */
private Deque<Profile> profiles;
/** 已加載的 Profile 隊列 */
private List<Profile> processedProfiles;
/** 是否有激活的 Profile */
private boolean activatedProfiles;
/** 保存每個 Profile 對應的屬性信息 */
private Map<Profile, MutablePropertySources> loaded;
private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
// 占位符處理器
this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);
// 設置默認的資源加載器
this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader();
/**
* 通過 ClassLoader 從所有的 `META-INF/spring.factories` 文件中加載出 PropertySourceLoader
* Spring Boot 配置了兩個屬性資源加載器:
* {@link PropertiesPropertySourceLoader} 加載 `properties` 和 `xml` 文件
* {@link YamlPropertySourceLoader} 加載 `yml` 和 `yaml` 文件
*/
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
}
}
屬性不多,上面都已經注釋了,在構造器中會通過 ClassLoader 從所有的 META-INF/spring.factories 文件中加載出 PropertySourceLoader,如下:
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
PropertiesPropertySourceLoader:加載 properties 和 xml 文件
YamlPropertySourceLoader:加載 yml 和 yaml 文件
3. load 方法
load() 方法,加載配置信息,並放入 Environment 環境對象中,如下:
void load() {
// 借助 FilteredPropertySource 執行入參中的這個 Consumer 函數
// 目的就是獲取 `defaultProperties` 默認值的 PropertySource,通常我們沒有設置,所以為空對象
FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
(defaultProperties) -> {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
// <1> 初始化 Profile 對象,也就是我們需要加載的 Spring 配置,例如配置的 JVM 變量:`dev`、`sit`、`uat`、`prod`
// 1. `java -jar xxx.jar --spring.profiles.active=dev` or `java -jar -Dspring.profiles.active=dev xxx.jar`,那么這里的 `profiles` 就會有一個 `null` 和一個 `dev`
// 2. `java -jar xxx.jar`,那么這里的 `profiles` 就會有一個 `null` 和一個 `default`
initializeProfiles();
// <2> 依次加載 `profiles` 對應的配置信息
// 這里先解析 `null` 對應的配置信息,也就是公共配置
// 針對上面第 `2` 種情況,如果公共配置指定了 `spring.profiles.active`,那么添加至 `profiles` 中,並移除 `default` 默認 Profile
// 所以后續和上面第 `1` 種情況一樣的處理
while (!this.profiles.isEmpty()) {
// <2.1> 將接下來的准備加載的 Profile 從隊列中移除
Profile profile = this.profiles.poll();
// <2.2> 如果不為 `null` 且不是默認的 Profile,這個方法名不試試取錯了??
if (isDefaultProfile(profile)) {
// 則將其添加至 Environment 的 `activeProfiles`(有效的配置)中,已存在不會添加
addProfileToEnvironment(profile.getName());
}
/**
* <2.3> 嘗試加載配置文件,並解析出配置信息,會根據 Profile 歸類,最終保存至 {@link this#loaded} 集合
* 例如會去加載 `classpath:/application.yml` 或者 `classpath:/application-dev.yml` 文件,並解析
* 如果 `profile` 為 `null`,則會解析出 `classpath:/application.yml` 中的公共配置
* 因為這里是第一次去加載,所以不需要檢查 `profile` 對應的配置信息是否存在
*/
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
// <2.4> 將已加載的 Profile 保存
this.processedProfiles.add(profile);
}
/**
* <3> 如果沒有指定 `profile`,那么這里嘗試解析所有需要的環境的配置信息,也會根據 Profile 歸類,最終保存至 {@link this#loaded} 集合
* 例如會去加載 `classpath:/application.yml` 文件並解析出各個 Profile 的配置信息
* 因為上面可能嘗試加載過,所以這里需要檢查 `profile` 對應的配置信息是否存在,已存在則不再添加
* 至於這一步的用途暫時還沒搞懂~
*/
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
/** <4> 將上面加載出來的所有配置信息從 {@link this#loaded} 集合添加至 Environment 中 */
addLoadedPropertySources();
// <5> 設置被激活的 Profile 環境
applyActiveProfiles(defaultProperties);
});
}
方法內部借助 FilteredPropertySource 執行入參中的這個 Consumer 函數,目的就是獲取 defaultProperties 默認值的 PropertySource,通常我們沒有設置,所以為空對象,如下:
static void apply(ConfigurableEnvironment environment, String propertySourceName, Set<String> filteredProperties,
Consumer<PropertySource<?>> operation) {
MutablePropertySources propertySources = environment.getPropertySources();
// 先獲取當前環境中 `defaultProperties` 的 PropertySource 對象,默認沒有,通常我們也不會配置
PropertySource<?> original = propertySources.get(propertySourceName);
if (original == null) {
// 直接調用 `operation` 函數
operation.accept(null);
return;
}
// 將這個當前環境中 `defaultProperties` 的 PropertySource 對象進行替換
// 也就是封裝成一個 FilteredPropertySource 對象,設置了幾個需要過濾的屬性
propertySources.replace(propertySourceName, new FilteredPropertySource(original, filteredProperties));
try {
// 調用 `operation` 函數,入參是默認值的 PropertySource
operation.accept(original);
}
finally {
// 將當前環境中 `defaultProperties` 的 PropertySource 對象還原
propertySources.replace(propertySourceName, original);
}
}
所以我們直接來看到 load() 方法中的 Consumer 函數,整個處理過程如下:
-
調用
initializeProfiles()方法,初始化 Profile 對象,也就是我們需要加載的 Spring 配置,例如配置的 JVM 變量:dev、sit、uat、prod-
java -jar xxx.jar --spring.profiles.active=devorjava -jar -Dspring.profiles.active=dev xxx.jar,那么這里的profiles就會有一個null和一個dev -
java -jar xxx.jar,那么這里的profiles就會有一個null和一個default
-
-
依次加載上一步得到的
profiles對應的配置信息,這里先解析null對應的配置信息,也就是公共配置針對上面第
1.2種情況,如果公共配置指定了spring.profiles.active,那么添加至profiles中,並移除default默認 Profile,所以后續和上面第1.1種情況一樣的處理,后面會講到-
將接下來的准備加載的 Profile 從隊列中移除
-
如果不為
null且不是默認的 Profile,這個方法名不試試取錯了??則將其添加至 Environment 的activeProfiles(有效的配置)中,已存在不會添加也就是保存激活的 Profile 環境
-
調用
load(..)重載方法,嘗試加載配置文件,並解析出配置信息,會根據 Profile 歸類,最終保存至this#loaded集合例如會去加載
classpath:/application.yml或者classpath:/application-dev.yml文件,並解析;如果profile為null,則會解析出classpath:/application.yml中的公共配置,因為這里是第一次去加載,所以不需要檢查profile對應的配置信息是否存在 -
將已加載的 Profile 保存
-
-
繼續調用
load(..)重載方法,如果沒有指定profile,那么這里嘗試解析所有需要的環境的配置信息,也會根據 Profile 歸類,最終保存至this#loaded集合例如會去加載
classpath:/application.yml文件並解析出各個 Profile 的配置信息;因為上面可能嘗試加載過,所以這里需要檢查profile對應的配置信息是否存在,已存在則不再添加,至於這一步的用途暫時還沒搞懂~ -
調用
addLoadedPropertySources()方法,將上面加載出來的所有配置信息從this#loaded集合添加至 Environment 中
上面的的 load(..) 重載方法中有一個 Consumer 函數,它的入參又有一個 Consumer 函數,第 2.3 和 3 步的入參不同,注意一下⏩
上面的整個過程有點繞,有點難懂,建議各位小伙伴自己調試代碼⏩
3.1 initializeProfiles 方法
initializeProfiles() 方法,初始化 Profile 對象,也就是我們需要加載的 Spring 配置,如下:
private void initializeProfiles() {
// The default profile for these purposes is represented as null. We add it
// first so that it is processed first and has lowest priority.
// <1> 先添加一個空的 Profile
this.profiles.add(null);
// <2> 從 Environment 中獲取 `spring.profiles.active` 配置
// 此時還沒有加載配置文件,所以這里獲取到的就是你啟動 `jar` 包時設置的 JVM 變量,例如 `-Dspring.profiles.active`
// 或者啟動 `jar` 包時添加的啟動參數,例如 `--spring.profiles.active=dev`
Set<Profile> activatedViaProperty = getProfilesFromProperty(ACTIVE_PROFILES_PROPERTY);
// <3> 從 Environment 中獲取 `spring.profiles.include` 配置
Set<Profile> includedViaProperty = getProfilesFromProperty(INCLUDE_PROFILES_PROPERTY);
// <4> 從 Environment 配置的需要激活的 Profile 們,不在上面兩個范圍內則屬於其他
List<Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, includedViaProperty);
// <5> 將上面找到的所有 Profile 都添加至 `profiles` 中(通常我們只在上面的第 `2` 步可能有返回結果)
this.profiles.addAll(otherActiveProfiles);
// Any pre-existing active profiles set via property sources (e.g.
// System properties) take precedence over those added in config files.
this.profiles.addAll(includedViaProperty);
// 這里主要設置 `activatedProfiles`,表示已有需要激活的 Profile 環境
addActiveProfiles(activatedViaProperty);
// <6> 如果只有一個 Profile,也就是第 `1` 步添加的一個空對象,那么這里再創建一個默認的
if (this.profiles.size() == 1) { // only has null profile
for (String defaultProfileName : this.environment.getDefaultProfiles()) {
Profile defaultProfile = new Profile(defaultProfileName, true);
this.profiles.add(defaultProfile);
}
}
}
過程如下:
-
先往
profile集合添加一個空的 Profile -
從 Environment 中獲取
spring.profiles.active配置,此時還沒有加載配置文件,所以這里獲取到的就是你啟動jar包時設置的 JVM 變量,例如-Dspring.profiles.active,或者啟動jar包時添加的啟動參數,例如--spring.profiles.active=dev在前面的 《SpringApplication 啟動類的啟動過程》 這篇文章的 6. prepareEnvironment 方法 小節的第
2步講過 -
從 Environment 中獲取
spring.profiles.include配置 -
從 Environment 配置的需要激活的 Profile 們,不在上面兩個范圍內則屬於其他
-
將上面找到的所有 Profile 都添加至
profiles中(通常我們只在上面的第2步可能有返回結果) -
如果只有一個 Profile,也就是第
1步添加的一個空對象,那么這里再創建一個默認的
3.2 load 重載方法1
load(Profile, DocumentFilterFactory, DocumentConsumer) 方法,加載指定 Profile 的配置信息,如果為空則解析出公共的配置
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
// <1> 先獲取 `classpath:/`、`classpath:/config/`、`file:./`、`file:./config/` 四個路徑
// <2> 然后依次遍歷,從該路徑下找到對應的配置文件,找到了則通過 `consumer` 進行解析,並添加至 `loaded` 中
getSearchLocations().forEach((location) -> {
// <2.1> 判斷是否是文件夾,這里好像都是
boolean isFolder = location.endsWith("/");
// <2.2> 是文件夾的話找到應用配置文件的名稱,可以通過 `spring.config.name` 配置進行設置
// Spring Cloud 中默認為 `bootstrap`,Spring Boot 中默認為 `application`
Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
// <2.3> 那么這里開始解析 `application` 配置文件了
names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
});
}
過程如下:
-
調用
getSearchLocations()方法,獲取classpath:/、classpath:/config/、file:./、file:./config/四個路徑private Set<String> getSearchLocations() { Set<String> locations = getSearchLocations(CONFIG_ADDITIONAL_LOCATION_PROPERTY); if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) { locations.addAll(getSearchLocations(CONFIG_LOCATION_PROPERTY)); } else { // 這里會得到 `classpath:/`、`classpath:/config/`、`file:./`、`file:./config/` 四個路徑 locations.addAll(asResolvedSet(ConfigFileApplicationListener.this.searchLocations, DEFAULT_SEARCH_LOCATIONS)); } return locations; } -
然后依次遍歷,從該路徑下找到對應的配置文件,找到了則通過
consumer進行解析,並添加至loaded中-
判斷是否是文件夾,這里好像都是
-
是文件夾的話找到應用配置文件的名稱,默認就是
application名稱private Set<String> getSearchNames() { // 如果通過 `spring.config.name` 指定了配置文件名稱 if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) { String property = this.environment.getProperty(CONFIG_NAME_PROPERTY); // 進行占位符處理,並返回設置的配置文件名稱 return asResolvedSet(property, null); } // 如果指定了 `names` 配置文件的名稱,則對其進行處理(占位符) // 沒有指定的話則去 `application` 默認名稱 return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES); } -
遍歷上一步獲取到
names,默認只有一個application,那么這里開始解析application配置文件了,調用的還是一個load(..)重載方法
-
總結下來就是這里會嘗試從 classpath:/、classpath:/config/、file:./、file:./config/ 四個文件夾下面解析 application 名稱的配置文件
3.3 load 重載方法2
load(String, String, Profile, DocumentFilterFactory, DocumentConsumer) 方法,加載 application 配置文件,加載指定 Profile 的配置信息,如果為空則解析出公共的配置
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
// <1> 如果沒有應用的配置文件名稱,則嘗試根據 `location` 進行解析,暫時忽略
if (!StringUtils.hasText(name)) {
for (PropertySourceLoader loader : this.propertySourceLoaders) {
if (canLoadFileExtension(loader, location)) {
load(loader, location, profile, filterFactory.getDocumentFilter(profile), consumer);
return;
}
}
// 拋出異常
}
Set<String> processed = new HashSet<>();
/**
* <2> 遍歷 PropertySourceLoader 對配置文件進行加載,這里有以下兩個:
* {@link PropertiesPropertySourceLoader} 加載 `properties` 和 `xml` 文件
* {@link YamlPropertySourceLoader} 加載 `yml` 和 `yaml` 文件
*/
for (PropertySourceLoader loader : this.propertySourceLoaders) {
// 先獲取 `loader` 的后綴,也就是說這里會總共會遍歷 4 次,分別處理不同后綴的文件
// 加上前面 4 種 `location`(文件夾),這里會進行 16 次加載
for (String fileExtension : loader.getFileExtensions()) {
// 避免重復加載
if (processed.add(fileExtension)) {
// 例如嘗試加載 `classpath:/application.yml` 文件
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
consumer);
}
}
}
}
過程如下:
-
如果沒有應用的配置文件名稱,則嘗試根據
location進行解析,暫時忽略 -
遍歷 PropertySourceLoader 對配置文件進行加載,回到 Loader 的構造方法中,會有
PropertiesPropertySourceLoader和YamlPropertySourceLoader兩個對象,前者支持properties和xml后綴,后者支持yml和yaml-
獲取 PropertySourceLoader 支持的后綴,然后依次加載對應的配置文件
也就是說四種后綴,加上前面四個文件夾,那么接下來每次 3.load 方法 都會調用十六次
loadForFileExtension(..)方法
-
3.4 loadForFileExtension 方法
loadForFileExtension(PropertySourceLoader, String, String, Profile, DocumentFilterFactory, DocumentConsumer) 方法,嘗試加載 classpath:/application.yml 配置文件,加載指定 Profile 的配置信息,如果為空則解析出公共的配置
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
// <1> 創建一個默認的 DocumentFilter 過濾器 `defaultFilter`
DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
// <2> 創建一個指定 Profile 的 DocumentFilter 過濾器 `profileFilter`
DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
// <3> 如果傳入了 `profile`,那么嘗試加載 `application-${profile}.yml`對應的配置文件
if (profile != null) {
// Try profile-specific file & profile section in profile file (gh-340)
// <3.1> 獲取 `profile` 對應的名稱,例如 `application-dev.yml`
String profileSpecificFile = prefix + "-" + profile + fileExtension;
// <3.2> 嘗試對該文件進行加載,公共配置
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
// <3.3> 嘗試對該文件進行加載,環境對應的配置
load(loader, profileSpecificFile, profile, profileFilter, consumer);
// Try profile specific sections in files we've already processed
// <3.4> 也嘗試從該文件中加載已經加載過的環境所對應的配置
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
// Also try the profile-specific section (if any) of the normal file
// <4> 正常邏輯,這里嘗試加載 `application.yml` 文件中對應 Profile 環境的配置
// 當然,如果 Profile 為空也就加載公共配置
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}
過程如下:
-
創建一個默認的 DocumentFilter 過濾器
defaultFilterprivate DocumentFilter getPositiveProfileFilter(Profile profile) { return (Document document) -> { // 如果沒有指定 Profile,那么 Document 中的 `profiles` 也得為空 // 也就是不能有 `spring.profiles` 配置,就是公共配置咯 if (profile == null) { return ObjectUtils.isEmpty(document.getProfiles()); } // 如果指定了 Profile,那么 Document 中的 `profiles` 需要包含這個 Profile // 同時,Environment 中也要接受這個 Document 中的 `profiles` return ObjectUtils.containsElement(document.getProfiles(), profile.getName()) && this.environment.acceptsProfiles(Profiles.of(document.getProfiles())); }; } -
創建一個指定 Profile 的 DocumentFilter 過濾器
profileFilter -
如果傳入了
profile,那么嘗試加載application-${profile}.yml對應的配置文件- 獲取
profile對應的名稱,例如application-dev.yml - 又調用
load(..)重載方法加載3.1步的配置文件,這里使用defaultFilter過濾器,找到公共的配置信息 - 又調用
load(..)重載方法加載3.1步的配置文件,這里使用profileFilter過濾器,找到指定profile的配置信息 - 也嘗試從該文件中加載已經加載過的環境所對應的配置,也就是說
dev的配置信息,也能在其他的application-prod.yml中讀取
- 獲取
-
正常邏輯,繼續調用
load(..)重載方法,嘗試加載application.yml文件中對應 Profile 環境的配置,當然,如果 Profile 為空也就加載公共配置
沒有什么復雜的邏輯,繼續調用重載方法
3.5 load 重載方法3
load(PropertySourceLoader, String, Profile, DocumentFilter,DocumentConsumer) 方法,嘗試加載配置文件,加載指定 Profile 的配置信息,如果為空則解析出公共的配置
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
DocumentConsumer consumer) {
try {
// <1> 通過資源加載器獲取這個文件資源
Resource resource = this.resourceLoader.getResource(location);
// <2> 如果文件資源不存在,那直接返回了
if (resource == null || !resource.exists()) {
return;
}
// <3> 否則,如果文件資源的后綴為空,跳過,直接返回
if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
return;
}
String name = "applicationConfig: [" + location + "]";
// <4> 使用 PropertySourceLoader 加載器加載出該文件資源中的所有屬性,並將其封裝成 Document 對象
// Document 對象中包含了配置文件的 `spring.profiles` 和 `spring.profiles.active` 屬性
// 一個文件不是對應一個 Document,因為在一個 `yml` 文件可以通過 `---` 來配置多個環境的配置,這里也就會有多個 Document
List<Document> documents = loadDocuments(loader, name, resource);
// <5> 如果沒有解析出 Document,表明該文件資源無效,跳過,直接返回
if (CollectionUtils.isEmpty(documents)) {
return;
}
List<Document> loaded = new ArrayList<>();
// <6> 通過 DocumentFilter 對 `document` 進行過濾,過濾出想要的 Profile 對應的 Document
// 例如入參的 Profile 為 `dev` 那么這里只要 `dev` 對應 Document
// 如果 Profile 為空,那么找到沒有 `spring.profiles` 配置 Document,也就是我們的公共配置
for (Document document : documents) {
if (filter.match(document)) {
// 如果前面還沒有激活的 Profile
// 那么這里嘗試將 Document 中的 `spring.profiles.active` 添加至 `profiles` 中,同時刪除 `default` 默認的 Profile
addActiveProfiles(document.getActiveProfiles());
addIncludedProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
// <7> 將需要的 Document 們進行倒序,因為配置在后面優先級越高,所以需要反轉一下
Collections.reverse(loaded);
// <8> 如果有需要的 Document
if (!loaded.isEmpty()) {
/**
* 借助 Lambda 表達式調用 {@link #addToLoaded} 方法
* 將這些 Document 轉換成 MutablePropertySources 保存至 {@link this#loaded} 集合中
*/
loaded.forEach((document) -> consumer.accept(profile, document));
}
} catch (Exception ex) {
throw new IllegalStateException("Failed to load property source from location '" + location + "'", ex);
}
}
過程如下:
-
通過資源加載器獲取這個文件資源,例如
classpath:/application.yml -
如果文件資源不存在,那直接返回了
-
否則,如果文件資源的后綴為空,跳過,直接返回
-
調用
loadDocuments(..)方法,使用 PropertySourceLoader 加載器加載出該文件資源中的所有屬性,並將其封裝成 Document 對象Document 對象中包含了配置文件的
spring.profiles和spring.profiles.active屬性,一個文件不是對應一個 Document,因為在一個yml文件可以通過---來配置多個環境的配置,這里也就會有多個 Document -
如果沒有解析出 Document,表明該文件資源無效,跳過,直接返回
-
通過 DocumentFilter 對
document進行過濾,過濾出想要的 Profile 對應的 Document例如入參的 Profile 為
dev那么這里只要dev對應 Document,如果 Profile 為空,那么找到沒有spring.profiles配置 Document,也就是我們的公共配置- 如果前面還沒有激活的 Profile,那么這里嘗試將 Document 中的
spring.profiles.active添加至profiles中,同時刪除default默認的 Profile
- 如果前面還沒有激活的 Profile,那么這里嘗試將 Document 中的
-
將需要的 Document 們進行倒序,因為配置在后面優先級越高,所以需要反轉一下
-
如果有需要的 Document,借助 Lambda 表達式調用
addToLoaded(..)方法,將這些 Document 轉換成 MutablePropertySources 保存至this#loaded集合中
邏輯沒有很復雜,找到對應的 application.yml 文件資源,解析出所有的配置,找到指定 Profile 對應的配置信息,然后添加到集合中
你要知道的是上面第 4 步得到的 Document 對象,例如 application.yml 中設置 dev 環境激活,有兩個 dev 和 prod 不同的配置,那么這里會得到三個 Document 對象
3.6 loadDocuments 方法
loadDocuments(PropertySourceLoader, String, Resource) 方法,從文件資源中加載出 Document 們,如下:
private List<Document> loadDocuments(PropertySourceLoader loader, String name, Resource resource)
throws IOException {
DocumentsCacheKey cacheKey = new DocumentsCacheKey(loader, resource);
// 嘗試從緩存中獲取
List<Document> documents = this.loadDocumentsCache.get(cacheKey);
if (documents == null) {
// 使用 PropertySourceLoader 加載器進行加載
List<PropertySource<?>> loaded = loader.load(name, resource);
// 將 PropertySource 轉換成 Document
documents = asDocuments(loaded);
// 放入緩存
this.loadDocumentsCache.put(cacheKey, documents);
}
return documents;
}
private List<Document> loadDocuments(PropertySourceLoader loader, String name, Resource resource)
throws IOException {
DocumentsCacheKey cacheKey = new DocumentsCacheKey(loader, resource);
// 嘗試從緩存中獲取
List<Document> documents = this.loadDocumentsCache.get(cacheKey);
if (documents == null) {
// 使用 PropertySourceLoader 加載器進行加載
List<PropertySource<?>> loaded = loader.load(name, resource);
// 將 PropertySource 轉換成 Document
documents = asDocuments(loaded);
// 放入緩存
this.loadDocumentsCache.put(cacheKey, documents);
}
return documents;
}
邏輯比較簡單,先通過 PropertySourceLoader 加載配置文件,例如 YamlPropertySourceLoader 加載 application.yml 配置文件
然后將加載出來的 PropertySource 屬性源對象們一一封裝成 Document 對象,同時放入緩存中
YamlPropertySourceLoader
org.springframework.boot.env.YamlPropertySourceLoader,yml 和 yaml 配置文件的加載器
public class YamlPropertySourceLoader implements PropertySourceLoader {
@Override
public String[] getFileExtensions() {
return new String[] { "yml", "yaml" };
}
@Override
public List<PropertySource<?>> load(String name, Resource resource) throws IOException {
// 如果不存在 `org.yaml.snakeyaml.Yaml` 這個 Class 對象,則拋出異常
if (!ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) {
throw new IllegalStateException(
"Attempted to load " + name + " but snakeyaml was not found on the classpath");
}
// 通過 Yaml 解析該文件資源
List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();
if (loaded.isEmpty()) {
return Collections.emptyList();
}
// 將上面獲取到的 Map 集合們一一封裝成 OriginTrackedMapPropertySource 對象
List<PropertySource<?>> propertySources = new ArrayList<>(loaded.size());
for (int i = 0; i < loaded.size(); i++) {
String documentNumber = (loaded.size() != 1) ? " (document #" + i + ")" : "";
propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber,
Collections.unmodifiableMap(loaded.get(i)), true));
}
return propertySources;
}
}
可以看到,主要就是通過 org.yaml.snakeyaml.Yaml 解析配置文件
3.7 addToLoaded 方法
addToLoaded(BiConsumer<MutablePropertySources, PropertySource<?>>, boolean) 方法,將加載出來的配置信息保存起來,如下:
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;
}
}
}
// 獲取 `loaded` 中該 Profile 對應的 MutablePropertySources 對象
MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
(k) -> new MutablePropertySources());
// 往這個 MutablePropertySources 對象中添加 Document 對應的 PropertySource
addMethod.accept(merged, document.getPropertySource());
};
}
往 loaded 中添加該 Profile 對應的 PropertySource 屬性源們
4. addLoadedPropertySources 方法
addLoadedPropertySources() 方法,將前面加載出來的所有 PropertySource 配置信息們添加到 Environment 環境中
private void addLoadedPropertySources() {
// 獲取當前 Spring 應用的 Environment 環境中的配置信息
MutablePropertySources destination = this.environment.getPropertySources();
// 將上面已加載的每個 Profile 對應的屬性信息放入一個 List 集合中 `loaded`
List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());
// 將 `loaded` 進行翻轉,因為寫在后面的環境優先級更高
Collections.reverse(loaded);
String lastAdded = null;
Set<String> added = new HashSet<>();
// 遍歷 `loaded`,將每個 Profile 對應的屬性信息按序添加到 Environment 環境中
for (MutablePropertySources sources : loaded) {
for (PropertySource<?> source : sources) {
if (added.add(source.getName())) {
// 放入上一個 PropertySource 的后面,優先默認配置
addLoadedPropertySource(destination, lastAdded, source);
lastAdded = source.getName();
}
}
}
}
過程如下:
- 獲取當前 Spring 應用的 Environment 環境中的配置信息
destination - 將上面已加載的每個 Profile 對應的屬性信息放入一個 List 集合中
loaded - 將
loaded進行翻轉,因為寫在后面的環境優先級更高❓❓❓前面不是翻轉過一次嗎?好吧,暫時忽略 - 遍歷
loaded,將每個 Profile 對應的 PropertySources 屬性信息按序添加到 Environment 環境中
5. applyActiveProfiles 方法
applyActiveProfiles(PropertySource<?> defaultProperties) 方法,設置被激活的 Profile 環境
private void applyActiveProfiles(PropertySource<?> defaultProperties) {
List<String> activeProfiles = new ArrayList<>();
// 如果默認的配置信息不為空,通常為 `null`
if (defaultProperties != null) {
Binder binder = new Binder(ConfigurationPropertySources.from(defaultProperties),
new PropertySourcesPlaceholdersResolver(this.environment));
activeProfiles.addAll(getDefaultProfiles(binder, "spring.profiles.include"));
if (!this.activatedProfiles) {
activeProfiles.addAll(getDefaultProfiles(binder, "spring.profiles.active"));
}
}
// 遍歷已加載的 Profile 對象,如果它不為 `null` 且不是默認的,那么添加到需要 `activeProfiles` 激活的隊列中
this.processedProfiles.stream().filter(this::isDefaultProfile).map(Profile::getName)
.forEach(activeProfiles::add);
// 設置 Environment 需要激活的環境名稱
this.environment.setActiveProfiles(activeProfiles.toArray(new String[0]));
}
邏輯比較簡單,例如我們配置了 spring.profiles.active=dev,那么這里將設置 Environment 被激活的 Profile 為 dev
總結
本文分析了 Spring Boot 加載 application.yml 配置文件並應用於 Spring 應用的 Environment 環境對象的整個過程,主要是借助於 Spring 的 ApplicationListener 事件監聽器機制,在啟動 Spring 應用的過程中,准備好 Environment 的時候會廣播 應用環境已准備好 事件,然后 ConfigFileApplicationListener 監聽到該事件會進行處理。
加載 application 配置文件的整個過程有點繞,嵌套有點深,想深入了解的話查看上面的內容,每個小節都進行了編號。
大致流程就是先加載出 application.yml 文件資源,然后找到需要的 Profile 對應的 PropertySource 屬性信息,包括公告配置,最后將這些 PropertySource 應用於 Environment。
