Spring Environment的載入

  • 2019 年 10 月 3 日
  • 筆記

 這節介紹environment,默認環境變數的載入以及初始化。

 之前在介紹spring啟動過程講到,第一步進行環境準備時就會初始化一個StandardEnvironment。下圖為Environment類圖的介面,可以分為4塊內容:

  1. ConversionService(藍):類型轉換服務

  2. PropertySource(綠):鍵值對數據源

  3. PropertyResolver(紅):鍵值對服務,包括類型轉換

  4. Environment(紫):環境配置數據服務

file

1.ConversionService

 提供了類型轉換服務,能將源目標轉換為目標類型,同時提供了管理功能,內部維護了各類型轉換映射關係。其實從ConversionService和ConverterRegistry介面就能看出該模組的功能,如下:

file

 ConversionService介面為主要的對外功能介面,提供查詢的能力。

file

 ConverterRegistry介面為主要的管理介面,提供添加和刪除的能力。而ConfigurableConversionService繼承自上面二者,則提供了Converter的CRUD功能。結構上也延續了Spring固有的風格,將執行介面作為主要功能對外提供單一的介面,再通過繼承的方式,以Configurable開頭的子介面,擴展出管理功能,使得責任分離更加立體。

 接下來是GenericConversionService類,該類提供了介面全部實現,下圖展示了其主要實現:

file

 GenericConversionService結構上可以說是一個小型的管理系統,內部維護了一個Converters對象,用於「底層」管理所有的GenericConverter。同時還維護了一個ConcurrentReferenceHashMap用於快取常用的GenericConverter。

 Converters在存儲GenericConverter時還進行了分類,如果GenericConverter有指定能夠解析的類別(ConvertiablePair:包括SourceType和TargetType)時,則使用一個LinkedHashMap按Key Value進行存儲,在存儲時會遍歷可解析的類別,將該GenericConverter追加到對應的Value列表末尾,因而可以看到該Map的Value是一個LinkedList。對應沒有指定能解析的類別的GenericConverter,則直接放到LinkedHashSet維護的集合中。

 Converters在查詢時會遍歷源類型和目標類型的組合結果,以查找匹配的目標GenericConverter對象。如下:

file

 對於getRegisteredConverter方法,會先使用Key從LinkedHashMap中查找是否有匹配的Converter,再遍歷相應的Value,查找到能處理的轉換器。若Map中無法查到,則遍歷LinkedHashSet,以查到到能處理的轉換器。

 由上知道,Converters在查找時存在多次遍歷列表的過程,在頻率過多時效率會比較低下,因而GenericConversionService內部維護了一個ConcurrentReferenceHashMap提供快取的功能,該Map提供了同ConcurrentHashMap相同的功能,但是能夠存儲對應的軟引用,從而能在記憶體不足時自動進行記憶體回收。在查到轉換器時,會先試著從快取中查找,如果獲取不到,則會轉而從Converters中查找,當從Converters中查找到後便會put到ConcurrentReferenceHashMap快取中。

 DefaultConversionService是一個單例,繼承自GenericConversionService,在初始化後自動添加了默認的轉換器,包括Scalar相關的、集合相關的等轉換器。

2.PropertySource

 PropertySource代表了一個包含鍵值對的數據源。從類定義上看,有一個表示數據源名字的name欄位,還有一個表示具體數據源泛型T的source欄位。而數據源的設置則是通過構造方法傳入的,同時方法提供了通過鍵名獲取鍵值的抽象方法getProperty。此外還有其他抽象方法,如containsProperty等。

 EnumerablePropertySource繼承自PropertySource,增加了getPropertyNames方法,要求子類返回記憶體持有的鍵名列表。同時實現了containsProperty方法,通過判斷所給的鍵名是否存在上述返回的鍵名列表中從而判斷是否包含該鍵名。

 MapPropertySource繼承自EnumerablePropertySource,顧名思義,內部通過Map維護各鍵值對內容。類似的還有PropertiesPropertySource,內部通過Properties維護各鍵值對內容。

 SystemEnvironmentPropertySource是MapPropertySource的裝飾器,繼承自MapPropertySource,為其添加了鍵名轉換功能,以應對環境變數、shell參數的環境。在通過鍵名獲取鍵值時,會先根據原鍵名進行查找,查找不到則通過對鍵進行轉換再嘗試查找,具體查找過程為:

  1. 通過name查找

  2. 將name中的 . 轉換為 _ 查找

  3. 將name中的 – 轉換為 _ 查找

  4. 將name中的 . 和 _ 轉換為 – 查找

  5. 將name轉換為大寫,再進行(1) – (4)的過程

 PropertySources的實現如下,擴展了PropertySource介面,將單個數據源的能力擴展到了多個。MutablePropertySources作為PropertySources的實現,內部維護了一個List對象,用以存儲多個數據源,並將自身的行為封裝為List。

file

3.PropertyResolver

 PropertyResolver定義了一系列介面,以提供了對外根據鍵名獲取相應值的功能,同時提供了類型轉換和佔位符替換的功能,是ConversionService和PropertySource的結合。ConfigurablePropertyResolver介面繼承自PropertyResolver介面,老規則,擴展了設置的功能,主要是設置類型轉換器和佔位符的相關屬性。

 AbstractPropertyResolver提供了除PropertySource功能外的其餘實現。使用DefaultConversionService作為默認的類型轉換實現,使用 ${ 和 } 作為佔位符的前後綴,使用:作為默認值分割符,同時引入PropertyPlaceholderHelper用於佔位符的解析和替換。而getProperty的實現則留到了了子類PropertySourcesPropertyResolver中,其引入了PropertySources用以維護多個鍵值對數據源。獲取指定屬性值過程如下:

file

 通過遍曆數據源的方式,查到對應的值後,會進行佔位符的替換,替換完佔位符後會進行類型的轉換。類型轉換直接用的DefaultConversionService,這個上面已經介紹過了,下面介紹佔位符替換。

 佔位符替換的功能是在PropertyResolver介面中定義的,分為嚴格和不嚴格模式,如下:

file

 resolvePlaceholders為不嚴格模式,如果沒法替換佔位符,則直接忽略,resolveRequiredPlaceholders為嚴格模式,如果佔位符沒法替換則會拋出異常。如上面說的,AbstractPropertyResolver實現時都委託給PropertyPlaceholderHelper的replacePlaceholders方法。

file

 如上,該方法要求傳入一個源字元串,同時提供一個PlaceholderResolver數據源,一遍解析出佔位符內容後能夠從數據源中獲取對應的值。為了保持類功能的單一職責,從而增加了一個內部介面PlaceholderResolver。上面提到,在這個模組中的鍵值對數據源都是由PropertySourcesPropertyResolver維護的,事實上上面方法截圖的實現中,getPropertyAsRawString方法也確實是由PropertySourcesPropertyResolver提供實現的,下面看下佔位符的解析。

file

 佔位符的解析過程如上流程,主要過程為:

  1. 根據${前綴得到startIndex

  2. 查找跟${前綴配對的}後綴,如${xxx${yy}z},得到第二個}後綴的下標endIndex

  3. 截取${和}中間的內容得到placeholder

  4. 由於placeholder的內容可能也可能包含佔位符,因而要遞歸處理placeholder,既佔位符可以嵌套,內層的結果可以當做外層的Key使用

  5. placeholder解析完後,將其作為Key從鍵值對源中獲取對應的值propVal

  6. 如果propVal值為空,則判斷是否存在:分割符,如果有分割符,則進行分割,並使用前端內容作為Key再次查找值。若該次查找結果不為空,則使用該次結果為propVal的值,否則使用第二段內容作為默認值

  7. 若第(5)/(6)步中propVal結果不為空,則判斷從鍵值對源中獲取的值是否也有佔位符,若有佔位符,則再次進行解析,若沒有,則將結果替換回原欄位中,更新startIndex,繼續下次解析。

  8. 若第(5)/(6)步中propVal結果不空,則會根據設置的解析模式來判斷下一步行為,如果未不嚴格模式,則跳過該次內容,更新startIndex,繼續下一次解析,若為嚴格模式,則拋出異常,流程結束。

 下面以一個例子進行演示,如下

file

輸出結果為:

file

 若將解析模式設置為嚴格模式則會拋出異常

4.Environment

 Environment繼承自PropertyResolver介面,增加了Profiles功能,即我們平時看到的,多環境特性,能夠在不同環境下載入不同的配置。ConfigurableEnvironment繼承自Environment,老規矩,又是添加了修改的擴展介面,同時增加了獲取系統參數的介面。另外,該介面也繼承自ConfigurablePropertyResolver,有了鍵值對數據源管理、獲取和處理的能力,集合Environment介面的功能,能夠達到在不同環境下通過載入不同配置源實現環境隔離的效果。

 AbstractEnvironment是ConfigurablePropertyResolver的實現,提供了默認的環境源default,同時內部組合使用PropertySourcesPropertyResolver作為PropertyResolver的實現。

 它還維護了一個MutablePropertySources對象,用於存儲多個數據源,在Context的父子上下文中,通過merge方法,能夠將父上文中的環境變數內容添加進來(在AbstractApplicationContext設置父Context時,會將父Environment進行合併)。同時還有一個方法customizePropertySources,會在構造方法中進行調用,開放給子類添加默認的鍵值對源,如下:

file

 最後是StandardEnvironment類,繼承自AbstractEnvironment,重寫了customizePropertySources方法,在該方法中添加了系統相關的屬性和應用環境變數相關的屬性的鍵值對源。如下

file

 而這兩個數據源來自於前面提到的PropertySource實現。其中,系統相關屬性SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME的數據源來源於System.getProperties(),而應用環境變數相關屬性SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME則來源於System.getenv()

file

個人公眾號:啊駝