前幾天的時候,項目里有一個需求,需要一個開關控制代碼中是否執行一段邏輯,於是理所當然的在yml文件中配置了一個屬性作為開關,再配合nacos就可以隨時改變這個值達到我們的目的,yml文件中是這樣寫的:
switch:turnOn:on程序中的代碼也很簡單,大致的邏輯就是下面這樣,如果取到的開關字段是on的話,那麼就執行if判斷中的代碼,否則就不執行:
@Value("${switch.turnOn}")privateStringon;@GetMapping("testn")publicvoidtest(){if("on".equals(on)){//TODO}}但是當代碼實際跑起來,有意思的地方來了,我們發現判斷中的代碼一直不會被執行,直到debug一下,才發現這裡的取到的值居然不是on而是true。

看到這,是不是感覺有點意思,首先盲猜是在解析yml的過程中把on作為一個特殊的值進行了處理,於是我乾脆再多測試了幾個例子,把yml中的屬性擴展到下面這些:
switch:turnOn:onturnOff:offturnOn2:'on'turnOff2:'off'再執行一下代碼,看一下映射後的值:

可以看到,yml中沒有帶引號的on和off被轉換成了true和false,帶引號的則保持了原來的值不發生改變。
到這裡,讓我忍不住有點好奇,為什麼會發生這種現象呢?於是強忍着困意翻了翻源碼,硬磕了一下SpringBoot加載yml配置文件的過程,終於讓我看出了點門道,下面我們一點一點細說!
因為配置文件的加載會涉及到一些SpringBoot啟動的相關知識,所以如果對這一塊不是很熟悉的同學,可以先提前先看一下Hydra在古早時期寫過一篇文章預熱一下。下面的介紹中,只會摘出一些對加載和解析配置文件比較重要的步驟進行分析,對其他無關部分進行了省略。
加載監聽器當我們啟動一個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的原理了。
思考那麼,下一個問題來了,既然yml文件解析中會做這樣的特殊處理,那麼如果換成properties配置文件怎麼樣呢?
sw.turnOn=onsw.turnOff=off執行一下程序,看一下結果:

可以看到,使用properties配置文件能夠正常讀取結果,看來是在解析的過程中沒有做特殊處理,至於解析的過程,有興趣的小夥伴可以自己去閱讀一下源碼。
- EOF -
優雅整潔的 Java 代碼命名技巧,風之極·淨化
起飛,會了這4個 Intellij IDEA 調試魔法,閱讀源碼都簡單了!
還在用策略模式解決 if-else?Map+函數式接口方法才是YYDS!
看完本文有收穫?請轉發分享給更多人
關注「ImportNew」,提升Java技能
點讚和在看就是最大的支持❤️