原始碼解讀 Spring Boot Profiles
前言
上文《一文掌握 Spring Boot Profiles》 是對 Spring Boot Profiles 的介紹和使用,因此本文將從原始碼角度探究 Spring Boot Profiles,讓我們看下 Spring Boot 底層是如何應用 Profiles 進行環境配置的隔離與生效的。
正文
首先,我們先來看下一個簡單的 Spring Boot 示例程式,
在主程式方法中,列印容器中獲取到 User 物件,它只有一個 name
屬性。
這裡 name
屬性引用了外部配置 user.username
的值,它是從配置檔案中讀取,這裡我定義兩個配置檔案設定該屬性,application.properties
application-prod.properties
。
有了配置檔案之後,啟動 SimapleSpringApplication
程式,我們首先可以看到日誌輸入:User Bean: User(name=one)
,由此可以看出程式讀取了 application.properties
的 user.username
配置。現在我們在 application.properties
中加入一行:
再次重啟啟動程式,可以看到控制檯如下日誌:
此時 User 物件的name屬性變成了 application-prod.properties
中定義的值,並且日誌提示 The following profiles are active: prod
prod
的Profile 在程式中啟用。接下來我們就從這個日誌入手,探究下這一切是如何發生的。
首先,根據 IDE 的全域性查詢功能,直接搜尋 The following profiles are active:
這些詞出現的位置,進行定位,可以找到這個日誌出現於 SpringApplication#logStartupProfileInfo
方法之中。
從日誌方法可以看出列印的 activeProfiles
來自上下文關聯的 environment
物件,再進一步檢視 logStartupProfileInfo
的呼叫位置,可以在 SpringApplication#prepareContext
我們重新執行程式,通過斷點方式攔截 SpringApplication#prepareContext
方法的指向, 獲取 environment
物件真實的型別為 StandardEnvironment,是 Environment 介面非Web環境的標準實現,儲存著一些應用配置和 Profiles 資訊,如果在Web環境下,context 關聯的就是 StandardServletEnvironment 型別的物件。
知道了日誌列印來自 StandardEnvironment 物件的 activeProfiles
屬性之後,就需要來看它是在什麼時間被賦值的了。繼續從呼叫鏈的上一級查詢,就到了 SpringApplication#run(java.lang.String...)
,這也是整個程式啟動的主要方法。
從圖中可以看出第一次獲取到的 environment
物件來自 SpringApplication#prepareEnvironment
內部生成, prepareEnvironment
方法內部首先通過 getOrCreateEnvironment
獲取一個基礎的 ConfigurableEnvironment
例項,然後對該例項物件初始化配置返回。
正在建立 environment
物件來自 SpringApplication#getOrCreateEnvironment
,看它的實現就可以驗證我們之前提到 environment
物件型別為 StandardEnvironment。
瞭解完 environment
的建立,接下來就關注 environment
的初始化了,這裡我們需要關注 listeners.environmentPrepared(environment)
這行程式碼,這裡的 listeners
為 SpringApplicationRunListeners 例項,是監聽器 SpringApplicationRunListener 的集合物件, SpringApplicationRunListener#environmentPrepared
方法中就是對每個 SpringApplicationRunListener 物件遍歷指向類似的 environmentPrepared
方法,當前集合中只有一個 EventPublishingRunListener 例項,檢視其 environmentPrepared
方法就可以看到它主要就是用於釋出包含 environment
例項的 ApplicationEnvironmentPreparedEvent
事件,讓其他所有監聽該事件的監聽器進行 environment
例項的配置。
事件物件 ApplicationEnvironmentPreparedEvent 還有一個 getEnvironment
方法獲取所傳遞的 environment
例項,我們可以通過看這個方法被使用的地方,獲取有哪些類在配置 environment
物件。
經過多次的檢視,從上圖可以定位到 ConfigFileApplicationListener 類內的方法對 environment
物件進行擴充套件,從命名可以看出這個監聽器跟配置檔案相關,比如它的一些常量屬性:CONFIG_NAME_PROPERTY
,CONFIG_LOCATION_PROPERTY
等。從類的註釋可以看出,Spring Boot 程式啟動所載入的 application.properties
或 application.yml
預設從四個路徑下載入,我們最常用的就是最後一種,它也可以告訴我們還可以把配置檔案放在哪,如何自定義載入配置檔案的路徑。
file:./config/:
file:./
classpath:config/
classpath:
將程式斷點設置於 ConfigFileApplicationListener#onApplicationEvent
方法之內,重新執行程式就看到程式此時執行到了 ConfigFileApplicationListener
類之中,內部經過多個方法呼叫從 onApplicationEvent
來到了 addPropertySources
方法,這個方法就是配置檔案的屬性源載入到 environment
環境去的。
這裡的 Loader
是 ConfigFileApplicationListener
類內部私有類,用於協調屬性源和配置 Profiles,我們再進一步跟蹤到它的 load 方法。
我們主要看這個方法中的是三個方法:
Loader#initializeProfiles
Loader#addProfileToEnvironment
Loader#load(Profile, DocumentFilterFactory, DocumentConsumer)
第一個方法 initializeProfiles
初始化 Profiles,給 profiles
屬性新增兩個元素,null
和 預設的Profile。
第二個方法 addProfileToEnvironment
就是將 Profile 新增到 environment
物件的 activeProfiles
裡,也就是最開始日誌列印的 activeProfiles
。
第三個方法就是載入配置檔案的資料來源和 Profies 相關的屬性。
進入 load
方法,這個方法內部通過不同配置路徑去嘗試執行另一個 load
方法載入配置檔案,這裡 name
就是配所要搜尋的配置檔名稱,預設為 application
。
由於我們的配置檔案在 ClassPath 下,所以只要留意當 location
為 classpath:/
的程式執行情況即可。
由於SpringBoot 配置檔案支援xml
,properties
, yml
格式,就需要不同 PropertySourceLoader 支援其檔案內容的載入:PropertiesPropertySourceLoader 支援 xml
,properties
檔案,YamlPropertySourceLoader 支援 yml 檔案,載入以 .yml
或 .yaml
字尾的檔案,Loader#loadForFileExtension
方法就完成了對這些配置檔案的載入。
我們示例程式只有 properties
檔案,所以只需要關注當 loader
為 PropertiesPropertySourceLoader時的 Loader#loadForFileExtension
方法的執行情況。
loadForFileExtension
內部呼叫另外一個載入配置檔案的 load
方法,當讀取到ClassPath下的application.properties
時,會執行到 Loader#loadDocuments
方法,這個方法就是把配置檔案作為文件進行載入,所有鍵值對配置都會以存在 PropertySource 之中,儲存到 Document 物件中。
!](http://ww3.sinaimg.cn/large/006tNc79ly1g5x80ivld4j31rk0qg130.jpg)
並且 documents
物件經過 Loader#asDocuments
方法關聯上 spring.profiles.active
屬性,profiles
屬性新增一個定義為 prod
的 Profile,為後面的 Environment 物件新增 Profile 做準備,到這裡預設的配置檔案 application.properties
載入完畢了,方法又回到了 Loader#load()
上。
有了新新增的 Profile,繼續進入迴圈,就會通過 Loader#addProfileToEnvironment
方法,為 environment
物件儲存啟用的 Profile,並且按照之前的邏輯,讀取名為 application-prod.properties
的配置檔案,命名方式可以從之前的 Loader#loadForFileExtension
的第462行就可以看出:
在 Loader#load()
方法讀取了所有配置檔案後,執行 Loader#addLoadedPropertySources
,將對應屬性源 PropertySource 儲存到 environment
物件中,並且 application-prod.properties
順序先於預設配置檔案,就是為了後面程式應用相同名稱配置的時候,優先採用元素位置在前的配置。
至此,所有配置檔案上的資料載入完儲存到了與當前上下文關聯的 environment
物件中,將 prod
作為 Active Profile 啟用特定環境配置的工作就完成了。
小結
雖然只是探究 Spring Boot 程式如何載入和應用 Profile,但通過這次原始碼分析,我們可以發現 SpringBoot 雖簡單易用,但是內部實現邏輯設計是比較複雜的,無論是資源的載入,資料的解析都有專門的元件類去處理,大量使用事件通知和設計模式,在分析原始碼時少不了一次又一次的執行斷點,不過這需要我們充分利用DE工具除錯功能,在錯綜複雜的程式碼中能更準確地定位目標。
推薦閱讀
- 一文掌握 Spring Boot Profiles
- 如何優雅關閉 Spring Boot 應用
- 需要介面管理的你瞭解一下?
- Java 之 Lombok 必知必會
- Java 微服務新生代之 Nacos