原創:微信公眾號
碼農參上
,歡迎分享,轉載請保留出處。
前幾天的時候,專案裡有一個需求,需要一個開關控制程式碼中是否執行一段邏輯,於是理所當然的在yml
檔案中配置了一個屬性作為開關,再配合nacos
就可以隨時改變這個值達到我們的目的,yml檔案中是這樣寫的:
switch:
turnOn: on
程式中的程式碼也很簡單,大致的邏輯就是下面這樣,如果取到的開關欄位是on
的話,那麼就執行if
判斷中的程式碼,否則就不執行:
@Value("${switch.turnOn}")
private String on;
@GetMapping("testn")
public void test(){
if ("on".equals(on)){
//TODO
}
}
但是當程式碼實際跑起來,有意思的地方來了,我們發現判斷中的程式碼一直不會被執行,直到debug一下,才發現這裡的取到的值居然不是on
而是true
。
看到這,是不是感覺有點意思,首先盲猜是在解析yml的過程中把on
作為一個特殊的值進行了處理,於是我乾脆再多測試了幾個例子,把yml中的屬性擴充套件到下面這些:
switch:
turnOn: on
turnOff: off
turnOn2: 'on'
turnOff2: 'off'
再執行一下程式碼,看一下對映後的值:
可以看到,yml中沒有帶引號的on
和off
被轉換成了true
和false
,帶引號的則保持了原來的值不發生改變。
到這裡,讓我忍不住有點好奇,為什麼會發生這種現象呢?於是強忍著睏意翻了翻原始碼,硬磕了一下SpringBoot載入yml配置檔案的過程,終於讓我看出了點門道,下面我們一點一點細說!
因為配置檔案的載入會涉及到一些SpringBoot啟動的相關知識,所以如果對SpringBoot啟動不是很熟悉的同學,可以先提前先看一下Hydra在古早時期寫過一篇Spring Boot零配置啟動原理預熱一下。下面的介紹中,只會摘出一些對載入和解析配置檔案比較重要的步驟進行分析,對其他無關部分進行了省略。
載入監聽器
當我們啟動一個SpringBoot程式,在執行SpringApplication.run()
的時候,首先在初始化SpringApplication
的過程中,載入了11個實現了ApplicationListener
介面的攔截器。
這11個自動載入的ApplicationListener
,是在spring.factories
中定義並通過SPI
擴充套件被載入的:
這裡列出的10個是在spring-boot
中載入的,還有剩餘的1個是在spring-boot-autoconfigure
中載入的。其中最關鍵的就是ConfigFileApplicationListener
,它和後面要講到的配置檔案的載入相關。
執行run方法
在例項化完成SpringApplication
後,會接著往下執行它的run
方法。
可以看到,這裡通過getRunListeners
方法獲取的SpringApplicationRunListeners
中,EventPublishingRunListener
繫結了我們前面載入的11個監聽器。但是在執行starting
方法時,根據型別進行了過濾,最終實際只執行了4個監聽器的onApplicationEvent
方法,並沒有我們希望看到的ConfigFileApplicationListener
,讓我們接著往下看。
當run
方法執行到prepareEnvironment
時,會建立一個ApplicationEnvironmentPreparedEvent
型別的事件,並廣播出去。這時所有的監聽器中,有7個會監聽到這個事件,之後會分別呼叫它們的onApplicationEvent
方法,其中就有了我們心心念唸的ConfigFileApplicationListener
,接下來讓我們看看它的onApplicationEvent
方法中做了什麼。
在方法的呼叫過程中,會載入系統自己的4個後置處理器以及ConfigFileApplicationListener
自身,一共5個後置處理器,並執行他們的postProcessEnvironment
方法,其他4個對我們不重要可以略過,最終比較關鍵的步驟是建立Loader
例項並呼叫它的load
方法。
載入配置檔案
這裡的Loader
是ConfigFileApplicationListener
的一個內部類,看一下Loader
物件例項化的過程:
在例項化Loader
物件的過程中,再次通過SPI擴充套件的方式載入了兩個屬性檔案載入器,其中的YamlPropertySourceLoader
就和後面的yml檔案的載入、解析密切關聯,而另一個PropertiesPropertySourceLoader
則負責properties
檔案的載入。建立完Loader
例項後,接下來會呼叫它的load
方法。
在load
方法中,會通過巢狀迴圈方式遍歷預設配置檔案存放路徑,再加上預設的配置檔名稱、以及不同配置檔案載入器對應解析的字尾名,最終找到我們的yml配置檔案。接下來,開始執行loadForFileExtension
方法。
在loadForFileExtension
方法中,首先將classpath:/application.yml
載入為Resource
檔案,接下來準備正式開始,呼叫了之前建立好的YamlPropertySourceLoader
物件的load
方法。
封裝Node
在load
方法中,開始準備進行配置檔案的解析與資料封裝:
load
方法中呼叫了OriginTrackedYmlLoader
物件的load
方法,從字面意思上我們也可以理解,它的用途是原始追蹤yml的載入器。中間一連串的方法呼叫可以忽略,直接看最後也是最重要的是一步,呼叫OriginTrackingConstructor
物件的getData
介面,來解析yml並封裝成物件。
在解析yml的過程中實際使用了Composer
構建器來生成節點,在它的getNode
方法中,通過解析器事件來建立節點。通常來說,它會將yml中的一組資料封裝成一個MappingNode
節點,它的內部實際上是一個NodeTuple
組成的List
,NodeTuple
和Map
的結構類似,由一對對應的keyNode
和valueNode
構成,結構如下:
好了,讓我們再回到上面的那張方法呼叫流程圖,它是根據文章開頭的yml檔案中實際內容內容繪製的,如果內容不同呼叫流程會發生改變,大家只需要明白這個原理,下面我們具體分析。
首先,建立一個MappingNode
節點,並將switch
封裝成keyNode
,然後再建立一個MappingNode
,作為外層MappingNode
的valueNode
,同時儲存它下面的4組屬性,這也是為什麼上面會出現4次迴圈的原因。如果有點困惑也沒關係,看一下下面的這張圖,就能一目瞭然瞭解它的結構。
在上圖中,又引入了一種新的ScalarNode
節點,它的用途也比較簡單,簡單String型別的字串用它來封裝成節點就可以了。到這裡,yml中的資料被解析完成並完成了初步的封裝,可能眼尖的小夥伴要問了,上面這張圖中為什麼在ScalarNode
中,除了value
還有一個tag
屬性,這個屬性是幹什麼的呢?
在介紹它的作用前,先說一下它是怎麼被確定的。這一塊的邏輯比較複雜,大家可以翻一下ScannerImpl
類fetchMoreTokens
方法的原始碼,這個方法會根據yml中每一個key
或value
是以什麼開頭,來決定以什麼方式進行解析,其中就包括了{
、[
、'
、%
、?
等特殊符號的情況。以解析不帶任何特殊字元的字串為例,簡要的流程如下,省略了一些不重要部分:
在這張圖的中間步驟中,建立了兩個比較重要的物件ScalarToken
和ScalarEvent
,其中都有一個為true
的plain
屬性,可以理解為這個屬性是否需要解釋,是後面獲取Resolver
的關鍵屬性之一。
上圖中的yamlImplicitResolvers
其實是一個提前快取好的HashMap,已經提前儲存好了一些Char
型別字元與ResolverTuple
的對應關係:
當解析到屬性on
時,取出首字母o
對應的ResolverTuple
,其中的tag
就是tag:yaml.org.2002:bool
。當然了,這裡也不是簡單的取出就完事了,後續還會對屬性進行正規表示式的匹配,看與regexp
中的值是否能對的上,檢查無誤時才會返回這個tag
。
到這裡,我們就解釋清楚了ScalarNode
中tag
屬性究竟是怎麼獲取到的了,之後方法呼叫層層返回,返回到OriginTrackingConstructor
父類BaseConstructor
的getData
方法中。接下來,繼續執行constructDocument
方法,完成對yml文件的解析。
呼叫構造器
在constructDocument
中,有兩步比較重要,第一步是推斷當前節點應該使用哪種型別的構造器,第二步是使用獲得的構造器來重新對Node
節點中的value
進行賦值,簡易流程如下,省去了迴圈遍歷的部分:
推斷構造器種類的過程也很簡單,在父類BaseConstructor
中,快取了一個HashMap,存放了節點的tag
型別到對應構造器的對映關係。在getConstructor
方法中,就使用之前節點中存入的tag
屬性來獲得具體要使用的構造器:
當tag
為bool
型別時,會找到SafeConstruct
中的內部類 ConstructYamlBool
作為構造器,並呼叫它的construct
方法例項化一個物件,來作為ScalarNode
節點的value
的值:
在construct
方法中,取到的val就是之前的on
,至於下面的這個BOOL_VALUES
,也是提前初始化好的一個HashMap,裡面提前存放了一些對應的對映關係,key是下面列出的這些關鍵字,value則是Boolean
型別的true
或false
:
到這裡,yml中的屬性解析流程就基本完成了,我們也明白了為什麼yml中的on
會被轉化為true
的原理了。至於最後,Boolean
型別的true
或false
是如何被轉化為的字串,就是@Value
註解去實現的了。
思考
那麼,下一個問題來了,既然yml檔案解析中會做這樣的特殊處理,那麼如果換成properties
配置檔案怎麼樣呢?
sw.turnOn=on
sw.turnOff=off
執行一下程式,看一下結果:
可以看到,使用properties
配置檔案能夠正常讀取結果,看來是在解析的過程中沒有做特殊處理,至於解析的過程,有興趣的小夥伴可以自己去閱讀一下原始碼。
那麼,今天就寫到這裡,我們下期見。
作者簡介,碼農參上,一個熱愛分享的公眾號,有趣、深入、直接,與你聊聊技術。個人微信DrHydra9,歡迎新增好友,進一步交流。