Springboot 載入配置檔案原始碼分析
本文的分析是基於springboot 2.2.0.RELEASE。
本篇文章的相關原始碼位置:https://github.com/wbo112/blogdemo/tree/main/springbootdemo/springboot-profiles
springboot載入配置檔案如application.yml是通過org.springframework.boot.context.config.ConfigFileApplicationListener
這個類來完成的。這個類的載入也是通過spring.factories檔案中來載入的。
ConfigFileApplicationListener
這個類同時實現了EnvironmentPostProcessor、ApplicationListener這兩個介面。
EnvironmentPostProcessor介面需要實現的postProcessEnvironment方法,這個方法主要就是用來對Environment來進行增強處理的。而Environment主要是用來表示當前應用程式執行環境的介面。在我們這裡來說讀取的配置檔案最終也會放到這裡面來。
簡單的呼叫關係就是下面的圖來,具體是怎麼調到onApplicationEvent的,比較簡單,就不和大家一起看了。直接看本次的重點吧。
下面的方法都是ConfigFileApplicationListener這個類中
在addPropertySources中首先會新增一個用來處理生成隨機數的RandomValuePropertySource,然後就會通過內部類Loader來載入配置檔案。所以我們本次的主要是看Loader類的load方法的執行流程
下面我們來具體看看load方法的執行流程
void load() {
//配置檔案的載入就是通過這裡來完成的。
//DEFAULT_PROPERTIES = "defaultProperties";
//LOAD_FILTERED_PROPERTY是一個set<String>,裡面有兩個元素{"spring.profiles.active","spring.profiles.include"}
FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
//下面這裡是一個lambda表示式,這裡暫時省略,後面具體呼叫到這裡我們再具體看。
(defaultProperties) -> {
......
});
}
上面的會呼叫到FilteredPropertySource的靜態方法,這個也比較簡單,就幾行程式碼。最終又會呼叫到上面我們的省略的lambda表示式
static void apply(ConfigurableEnvironment environment, String propertySourceName, Set<String> filteredProperties,
Consumer<PropertySource<?>> operation) {
//這個是獲取到當前程式的執行時環境。主要就是當前系統的環境變數、當前程式的環境變數等等,下面放個圖一起看看
MutablePropertySources propertySources = environment.getPropertySources();
//這裡是從執行時環境中查詢key=defaultProperties的屬性值,一般情況下,我們沒有配置這個屬性,獲取到的original就是null,就會走到下面的if分支中,回到我們的lambda表示式中,我們就看這個分支吧
PropertySource<?> original = propertySources.get(propertySourceName);
if (original == null) {
operation.accept(null);
return;
}
propertySources.replace(propertySourceName, new FilteredPropertySource(original, filteredProperties));
try {
operation.accept(original);
}
finally {
propertySources.replace(propertySourceName, original);
}
}
我們看看上面的MutablePropertySources propertySources = environment.getPropertySources();裡面的內容
可以看到它裡面有個propertySourceList,裡面有6個物件,裡面具體內容不開啟看了。我們最終讀取的配置檔案也會新增到這個列表裡面去
我們再回到上面的程式碼,看看lambda表示式中的呼叫吧
//這個就是上面load方法中我們之前省略掉的lambda表示式了
//這裡的入參defaultProperties是null
(defaultProperties) -> {
//這裡定義一個profiles,用來存放我們需要載入的profile的名字。
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
//這個方法首先會在profiles中新增一個null的空物件。代表一個全域性的profile,這個它一定會載入。
//後面會繼續從environment中查詢屬性為"spring.profiles.active","spring.profiles.include"的值通過屬性繫結的形式轉化成profile,也新增到profiles中來,我們這裡沒有定義這兩個屬性,所以也就不會新增了
//繼續判斷profiles的size,如果只有一個null的話,會在裡面新增一個名為default的profile,作為預設的profile
initializeProfiles();
//到這裡我們的profiles中已經有兩個物件了,一個全域性的null,一個預設的default。
//如果我們沒有定義自己的profile,那就會使用保留預設的;
//如果我們定義了自己的profile,就會刪除掉預設的名為default的profile
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
//首先這裡的profile的是null,不會進入下面的if分支
if (isDefaultProfile(profile)) {
//這裡會判斷如果不是預設的profile,就會加入到environment啟用的Profile列表中
addProfileToEnvironment(profile.getName());
}
//在這裡就會去載入配置檔案。
//這裡有3個引數
//1. 我們本次的profile
//2. 是個lambda表示式,主要是對要載入的配置檔案進行過濾。如果不符合需要,就不會載入
//3. 也是個lambda表示式,主要是是解析後的文件進行一個處理(加入到臨時列表中)
//這個方法比較重要。我們進這個方法去看看
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
addLoadedPropertySources();
applyActiveProfiles(defaultProperties);
});
//這個方法就是從指定的位置去查詢配置檔案進行載入
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
//getSearchLocations()獲取的要查詢配置檔案的目錄位置
//預設的會從下面4個位置去查詢file:./config/, file:./, classpath:/config/, classpath:/
getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
//由於我們上面的查詢路徑都是/結束的,所以查詢的是目錄,這裡會返回需要查詢的檔名application
Set<String> names = isFolder ? getSearchNames() : NO_SEARCH_NAMES;
//下面的方法就是從指定位置,使用profile,根據指定的名字去查詢配置檔案進行載入
names.forEach((name) -> load(location, name, profile, filterFactory, consumer));
});
}
上面看到getSearchLocations(),getSearchNames()分別是指定配置檔案查詢位置和具體的檔名。在ConfigFileApplicationListener類中也提供了對應的set方法,說明我們也可以根據自己的需要來進行指定。
從上面也能看到我們idea開發中在resource目錄中配置的application.yml中目錄和檔名分別是通過getSearchLocations()
和getSearchNames()
指定的。
從上面也能看到預設會在file:./config/, file:./, classpath:/config/, classpath:/
這4個位置去尋找檔名為application
的配置檔案。下面就是具體去查詢並載入配置檔案了
private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
//我們這裡的name ="application",所以就不會走到這個分支
if (!StringUtils.hasText(name)) {
......
}
Set<String> processed = new HashSet<>();
// this.propertySourceLoaders就是具體來負責載入配置檔案的。它是在ConfigFileApplicationListener構造方法中賦值的,
//具體的程式碼是下面這行
//this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,getClass().getClassLoader());
//當前的有兩個類PropertiesPropertySourceLoader,YamlPropertySourceLoader
//PropertiesPropertySourceLoader來載入字尾"properties", "xml" 的配置檔案
//YamlPropertySourceLoader來載入字尾為"yml", "yaml"的配置檔案
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String fileExtension : loader.getFileExtensions()) {
if (processed.add(fileExtension)) {
//具體在這裡就會遍歷PropertySourceLoader,來載入配置檔案
//loader就是PropertySourceLoader
//location + name就是檔案字首了。如file:./config/application
//"." + fileExtension是檔案字尾
//後面幾個引數都是入口傳入的
loadForFileExtension(loader, location + name, "." + fileExtension, profile, filterFactory,
consumer);
}
}
}
}
在這裡就是分別使用PropertiesPropertySourceLoader,YamlPropertySourceLoader去做具體的載入解析了
private void loadForFileExtension(PropertySourceLoader loader, String prefix, String fileExtension,
Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
//這裡是生成文件過濾器
//第一次進來的profile==null,不會走進下面的if分支
//下面這有兩個DocumentFilter,它們有什麼區別呢?
//第一個defaultFilter傳入的是null,也就是說如果當前載入的配置檔案中如果沒有spring.profiles這個屬性,那就會被載入
//第二個profileFilter傳入的是profile,那就需要配置檔案中的spring.profiles包含了當前的profile,且處於啟用狀態
DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
if (profile != null) {
// Try profile-specific file & profile section in profile file (gh-340)
//這裡就會去查詢類似我們工程中的配置檔案application-test.properties了
String profileSpecificFile = prefix + "-" + profile + fileExtension;
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
load(loader, profileSpecificFile, profile, profileFilter, consumer);
// Try profile specific sections in files we've already processed
//這個情況就是類似我們配置檔案application-test.properties的情況了
//雖然profile==test時會去載入application-test.properties,但是由於application-test.properties配置檔案中的 //spring.profiles=sitdba,是不能被profile=test的profileFilter匹配到的,
//但是可以被profiles=sitdbaprofileFilter匹配匹配到,也就是會在下面的for迴圈中被載入到
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
String previouslyLoaded = prefix + "-" + processedProfile + fileExtension;
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
//第一次profile==null,就會載入類似application.yml這種,檔名中沒有包含profile的配置檔案
// Also try the profile-specific section (if any) of the normal file
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}
在這裡就是具體拼接檔名:如application.yml,application-xxx.properties這種配置檔案具體去載入了
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter,
DocumentConsumer consumer) {
try {
//這裡就是使用資源載入器去載入指定位置上的資源,也就是載入我們的配置檔案。
//如果找不到配置檔案就直接返回,如果能找到就進行後面配置檔案的載入了
Resource resource = this.resourceLoader.getResource(location);
if (resource == null || !resource.exists()) {
if (this.logger.isTraceEnabled()) {
StringBuilder description = getDescription("Skipped missing config ", location, resource,
profile);
this.logger.trace(description);
}
return;
}
if (!StringUtils.hasText(StringUtils.getFilenameExtension(resource.getFilename()))) {
if (this.logger.isTraceEnabled()) {
StringBuilder description = getDescription("Skipped empty config extension ", location,
resource, profile);
this.logger.trace(description);
}
return;
}
String name = "applicationConfig: [" + location + "]";
//這裡會將配置檔案轉化成Document。
//yml檔案中會用---進行區分成不同的檔案,所以這裡就是一個list,表示載入多個
List<Document> documents = loadDocuments(loader, name, resource);
//例如一個空的檔案,就會走到這裡,返回
if (CollectionUtils.isEmpty(documents)) {
if (this.logger.isTraceEnabled()) {
StringBuilder description = getDescription("Skipped unloaded config ", location, resource,
profile);
this.logger.trace(description);
}
return;
}
List<Document> loaded = new ArrayList<>();
//這裡會對Document進行過濾,對於符合條件的進行後續處理
for (Document document : documents) {
//這個過濾的話主要有兩點
//1.如果profile是null,那麼就需要document的document.getProfiles()為空,就是配置檔案中沒有spring.profiles
//2.如果profile不是null,那麼就判斷document的profiles中包含profile,且當前啟用的profile包含了document的profile
if (filter.match(document)) {
//這個是查詢document中的spring.profiles.active加入到profiles中,在入口的lambda表示式就會繼續從profiles中去遍歷查詢配置檔案
//在這個方法中只會新增一次,後面再新增的話,判斷之前已經有新增,就會直接返回。同時在新增完了啟用的profile後,會刪除掉開始加入的名為default的預設的profile
addActiveProfiles(document.getActiveProfiles());
//這個是通過spring.profiles.include屬性新增引入的外部檔案的profile
addIncludedProfiles(document.getIncludeProfiles());
//將document加入到loaded列表中
loaded.add(document);
}
}
//這裡對上面的document順序進行反轉
Collections.reverse(loaded);
if (!loaded.isEmpty()) {
//在這裡會將document載入到成員變數loaded(它的結構是Map<Profile, MutablePropertySources>,下面的圖就是當前loaded中的內容)中
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);
}
}
從上面的圖上也能看到application-test.propertie並沒有在profile=test下面,而是在profile=sitdba下面
我們再次回到之前開頭的位置
(defaultProperties) -> {
this.profiles = new LinkedList<>();
this.processedProfiles = new LinkedList<>();
this.activatedProfiles = false;
this.loaded = new LinkedHashMap<>();
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
if (isDefaultProfile(profile)) {
addProfileToEnvironment(profile.getName());
}
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
//將啟用並已經解析過的profile新增到列表中,最後會將這個列表中的profile作為environment中啟用的profile
this.processedProfiles.add(profile);
}
//這個是什麼場景呢?
load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
//這裡就會將我們載入的配置檔案都新增到this.environment.getPropertySources()屬性上
addLoadedPropertySources();
//將上面的this.processedProfiles作為啟用的profile
applyActiveProfiles(defaultProperties);
}
上面是我們的application.yml。
-
圖1標註的順序也是很重要的,如果寫成sitdba,test。那樣是不會載入application-test.properties這個配置檔案的。
-
圖2標註的dev這個profile並不會被載入。原因我們上面的也說過了,因為
addActiveProfiles
只會新增一次。
profile也可以通過它來過濾我們不同環境中載入的不同的bean,如下圖這樣
具體的過濾是通過ProfileCondition來實現的。也是檢視當前profile註解中的值是否屬於當前啟用的profile
最終所有的配置檔案屬性等都會新增到Environment中,關於Environment,下篇文章和大家一起看看它的作用吧。