在講 Environment(後續簡稱 Env)之前,我們可以先講講 Property-Resolver。它是 Env 的父介面,也是屬性檢索與解析的頂層核心介面。主要用途包含:統一屬性查詢入口(省去手動呼叫 System.getenv 與 System.getProperty ...等不同來源的查找成本)、動態解析 ${...} 佔位符、以及透過 Conversion-Service 進行資料型態的自動轉換(Spring MVC 裡處理 Request 轉 DTO 的時候就會看到它的身影)。

Env 介面可以理解成一個 Property-Resolver 的擴充:除了擁有屬性讀取、屬性解析的能力以外,Env 還擴充了 Profile 的環境隔離機制。可以用來管理當前啟用的環境標籤(test、dev、prod ...等),也可以搭配 Spring 框架提供的 @Profile 註解,決定哪些 Bean 能夠在當前的環境下被註冊進容器。

Standard-Environment(Standard Env)是 Env 介面裡面的核心實作,基本上無論是傳統的 MVC web、現代的 Spring Boot web、響應式的 WebFlux、或 non-web 的 Spring boot 應用,底層的 Env 實體類別都直接(or 間接)繼承了 Standard Env。Standard Env 內部最關鍵的模板方法是 customize-Property-Sources。該方法預設掛載了 2 個最常用的屬性源:JVM 啟動參數(system-Properties)和 OS 底層的環境變數(system-Environment)。

透過這個預設掛載,我們可以在不用多做設定前提下,在 Spring 啟動初期拿到各種環境變數。此外,我們日常開發中用到的 application.properties(或 application.yml),在定義上也算是一種屬性源。這也是為什麼我們可以在設定檔内自定義環境變數,或者用 spring.profiles.active 讓 @Profile 條件裝配正確生效的原因。
但 application.xxx 類型的屬性源,實際的定位與讀取並不是由 Env 自主完成,背後仍要仰賴昨天提到的 Resource-Loader 幫忙:先透過 Resource-Loader 的 getResource 方法,把指定路徑轉換成 Resource 物件,後面才能進一步包裝成 Property-Source,並加進 Environment。兩者算是分工合作的關係。
說到這裡,可以多提到一個問題:當不同的屬性源都宣告了相同的變數(例如不同檔案位置的 application.yml 都設定了 server.port)Spring 這邊又是怎麼進行挑選的呢?簡單來說,Standard env 的父類(AbstractEnvironment)實作了一個 Env 的衍生介面 —— Configurable-Environment(Config Env)。
該介面提供了一個 getPropertySources 方法,可以按照優先順序打印所有的屬性源。預設的順序大致是命令列參數、JVM 啟動參數、OS 環境變數、外部配置檔、最後才是 jar 裡面的 application.yml。優先級是以改動的成本(或者說維護的成本?)來界定的,基本上改動成本越低、操作越即時的屬性源,優先級就會越高:像是上線發現有問題需要臨時熱修,短期的解決方法就可以用 Cmd 先快速墊一下,不需要重新經歷 mvn package、重建 docker/vm 環境 ...等耗時操作。
除了屬性的解析與配置以外,Env 另一個重要的功能是 Profile 的環境隔離機制。這個機制主要實作在 Env 介面的 profiles 相關方法上,具體的實作出現在 Abstract-Environment(Abstract Env),Abstract Env 內部有兩個集合參數(activeProfiles 和 defaultProfiles)來判定系統運行時生效的 Profile 狀態。
具體來說,activeProfiles 代表的是使用者「主動宣告」要啟用的環境標籤,預設為 empty Set,但只要我們有設定環境標籤,使集合非空,@Profile 的判斷條件就會以它為準;defaultProfiles 則扮演 fallback 的角色:預設的集合內會有一個 "default" 標籤,用來註冊帶有 @Profile("default") 註解的 components,藉此保證程式在沒有指定環境的狀況下也能正常啟動。
@Profile 本質上是 @Conditional 的一種語法糖:在組件掃描階段、Scanner 準備把某個 BD 註冊進 Registry 之前,Spring 會判斷一次 BD 的 metadata 是否帶有 @Profile 註解?有的話註解中的值是什麼?Env 裡當前啟用的 Profiles 又是哪些?如果註解中的值包含在 Env 啟用的值內,就會讓這個 BD 繼續走正常的註冊流程,否則就直接跳過,略過註冊。
此外,@Profile 的 value 除了單純填入環境名稱以外,也支援簡單的邏輯運算:可以用 ! 代表 not、用 | 代表 or、用 & 代表 and,也可以用小括號處理一些複合類型的環境判斷,只不過 Profile 內部的 and 跟 or 並不是常見的「and 優先級比 or 高」。準確一點講,Spring 並沒有在這裡定義誰的優先級比較高,因此如果一個判斷內同時有這兩個符號出現,就需要用小括號決定誰先誰後。
最後,可以回頭說一下之前提過的 BFPP:Spring 有一個負責解析 ${...} 後置處理器(Property-Sources-Placeholder-Configurer,簡稱 Configurer),它是讓 Env 真正發揮作用的經典場景之一:Configurer 在容器啟動之初,會先從 Environment 讀取完整的設定清單,並在容器層級提供一個統一的字串解析機制,使不同位置的 ${...} 可以用同一套解析機制去替換數值。這也算是一個經典的 Spring 分工實例:Env 組件負責「管理與排序設定」、Configurer BFPP 則負責「在 Bean 建立時把值替換進去。」

期待後續各位的閱讀與分享,我是 Pax,我們明天見。