SpringBoot解析yml全流程詳解
背景
前幾天的時候,項(xiàng)目里有一個需求,需要一個開關(guān)控制代碼中是否執(zhí)行一段邏輯,于是理所當(dāng)然的在yml
文件中配置了一個屬性作為開關(guān),再配合nacos
就可以隨時改變這個值達(dá)到我們的目的,yml文件中是這樣寫的:
switch: turnOn: on
程序中的代碼也很簡單,大致的邏輯就是下面這樣,如果取到的開關(guān)字段是on
的話,那么就執(zhí)行if
判斷中的代碼,否則就不執(zhí)行:
@Value("${switch.turnOn}") private String on; @GetMapping("testn") public void test(){ if ("on".equals(on)){ //TODO } }
但是當(dāng)代碼實(shí)際跑起來,有意思的地方來了,我們發(fā)現(xiàn)判斷中的代碼一直不會被執(zhí)行,直到debug一下,才發(fā)現(xiàn)這里的取到的值居然不是on
而是true
。
看到這,是不是感覺有點(diǎn)意思,首先盲猜是在解析yml的過程中把on
作為一個特殊的值進(jìn)行了處理,于是我干脆再多測試了幾個例子,把yml中的屬性擴(kuò)展到下面這些:
switch: turnOn: on turnOff: off turnOn2: 'on' turnOff2: 'off'
再執(zhí)行一下代碼,看一下映射后的值:
可以看到,yml中沒有帶引號的on
和off
被轉(zhuǎn)換成了true
和false
,帶引號的則保持了原來的值不發(fā)生改變。
到這里,讓我忍不住有點(diǎn)好奇,為什么會發(fā)生這種現(xiàn)象呢?于是強(qiáng)忍著困意翻了翻源碼,硬磕了一下SpringBoot加載yml配置文件的過程,終于讓我看出了點(diǎn)門道,下面我們一點(diǎn)一點(diǎn)細(xì)說!
因?yàn)榕渲梦募募虞d會涉及到一些SpringBoot啟動的相關(guān)知識,所以如果對這一塊不是很熟悉的同學(xué),可以先提前先看一下Hydra在古早時期寫過一篇文章預(yù)熱一下。下面的介紹中,只會摘出一些對加載和解析配置文件比較重要的步驟進(jìn)行分析,對其他無關(guān)部分進(jìn)行了省略。
加載監(jiān)聽器
當(dāng)我們啟動一個SpringBoot程序,在執(zhí)行SpringApplication.run()
的時候,首先在初始化SpringApplication
的過程中,加載了11個實(shí)現(xiàn)了ApplicationListener
接口的攔截器。
這11個自動加載的ApplicationListener
,是在spring.factories
中定義并通過SPI
擴(kuò)展被加載的:
這里列出的10個是在spring-boot
中加載的,還有剩余的1個是在spring-boot-autoconfigure
中加載的。其中最關(guān)鍵的就是ConfigFileApplicationListener
,它和后面要講到的配置文件的加載相關(guān)。
執(zhí)行run方法
在實(shí)例化完成SpringApplication
后,會接著往下執(zhí)行它的run
方法。
可以看到,這里通過getRunListeners
方法獲取的SpringApplicationRunListeners
中,EventPublishingRunListener
綁定了我們前面加載的11個監(jiān)聽器。但是在執(zhí)行starting
方法時,根據(jù)類型進(jìn)行了過濾,最終實(shí)際只執(zhí)行了4個監(jiān)聽器的onApplicationEvent
方法,并沒有我們希望看到的ConfigFileApplicationListener
,讓我們接著往下看。
當(dāng)run
方法執(zhí)行到prepareEnvironment
時,會創(chuàng)建一個ApplicationEnvironmentPreparedEvent
類型的事件,并廣播出去。這時所有的監(jiān)聽器中,有7個會監(jiān)聽到這個事件,之后會分別調(diào)用它們的onApplicationEvent
方法,其中就有了我們心心念念的ConfigFileApplicationListener
,接下來讓我們看看它的onApplicationEvent
方法中做了什么。
在方法的調(diào)用過程中,會加載系統(tǒng)自己的4個后置處理器以及ConfigFileApplicationListener
自身,一共5個后置處理器,并執(zhí)行他們的postProcessEnvironment
方法,其他4個對我們不重要可以略過,最終比較關(guān)鍵的步驟是創(chuàng)建Loader
實(shí)例并調(diào)用它的load
方法。
加載配置文件
這里的Loader
是ConfigFileApplicationListener
的一個內(nèi)部類,看一下Loader
對象實(shí)例化的過程:
在實(shí)例化Loader
對象的過程中,再次通過SPI擴(kuò)展的方式加載了兩個屬性文件加載器,其中的YamlPropertySourceLoader
就和后面的yml文件的加載、解析密切關(guān)聯(lián),而另一個PropertiesPropertySourceLoader
則負(fù)責(zé)properties
文件的加載。創(chuàng)建完Loader
實(shí)例后,接下來會調(diào)用它的load
方法。
在loadForFileExtension
方法中,首先將classpath:/application.yml
加載為Resource
文件,接下來準(zhǔn)備正式開始,調(diào)用了之前創(chuàng)建好的YamlPropertySourceLoader
對象的load
方法。
封裝Node
在load
方法中,開始準(zhǔn)備進(jìn)行配置文件的解析與數(shù)據(jù)封裝:
load
方法中調(diào)用了OriginTrackedYmlLoader
對象的load
方法,從字面意思上我們也可以理解,它的用途是原始追蹤yml的加載器。中間一連串的方法調(diào)用可以忽略,直接看最后也是最重要的是一步,調(diào)用OriginTrackingConstructor
對象的getData
接口,來解析yml并封裝成對象。
在解析yml的過程中實(shí)際使用了Composer
構(gòu)建器來生成節(jié)點(diǎn),在它的getNode
方法中,通過解析器事件來創(chuàng)建節(jié)點(diǎn)。通常來說,它會將yml中的一組數(shù)據(jù)封裝成一個MappingNode
節(jié)點(diǎn),它的內(nèi)部實(shí)際上是一個NodeTuple
組成的List
,NodeTuple
和Map
的結(jié)構(gòu)類似,由一對對應(yīng)的keyNode
和valueNode
構(gòu)成,結(jié)構(gòu)如下:
好了,讓我們再回到上面的那張方法調(diào)用流程圖,它是根據(jù)文章開頭的yml文件中實(shí)際內(nèi)容內(nèi)容繪制的,如果內(nèi)容不同調(diào)用流程會發(fā)生改變,大家只需要明白這個原理,下面我們具體分析。
首先,創(chuàng)建一個MappingNode
節(jié)點(diǎn),并將switch
封裝成keyNode
,然后再創(chuàng)建一個MappingNode
,作為外層MappingNode
的valueNode
,同時存儲它下面的4組屬性,這也是為什么上面會出現(xiàn)4次循環(huán)的原因。如果有點(diǎn)困惑也沒關(guān)系,看一下下面的這張圖,就能一目了然了解它的結(jié)構(gòu)。
在上圖中,又引入了一種新的ScalarNode
節(jié)點(diǎn),它的用途也比較簡單,簡單String類型的字符串用它來封裝成節(jié)點(diǎn)就可以了。到這里,yml中的數(shù)據(jù)被解析完成并完成了初步的封裝,可能眼尖的小伙伴要問了,上面這張圖中為什么在ScalarNode
中,除了value
還有一個tag
屬性,這個屬性是干什么的呢?
在介紹它的作用前,先說一下它是怎么被確定的。這一塊的邏輯比較復(fù)雜,大家可以翻一下ScannerImpl
類fetchMoreTokens
方法的源碼,這個方法會根據(jù)yml中每一個key
或value
是以什么開頭,來決定以什么方式進(jìn)行解析,其中就包括了{
、[
、'
、%
、?
等特殊符號的情況。以解析不帶任何特殊字符的字符串為例,簡要的流程如下,省略了一些不重要部分:
在這張圖的中間步驟中,創(chuàng)建了兩個比較重要的對象ScalarToken
和ScalarEvent
,其中都有一個為true
的plain
屬性,可以理解為這個屬性是否需要解釋,是后面獲取Resolver
的關(guān)鍵屬性之一。
上圖中的yamlImplicitResolvers
其實(shí)是一個提前緩存好的HashMap,已經(jīng)提前存儲好了一些Char
類型字符與ResolverTuple
的對應(yīng)關(guān)系:
當(dāng)解析到屬性on
時,取出首字母o
對應(yīng)的ResolverTuple
,其中的tag
就是tag:yaml.org.2002:bool
。當(dāng)然了,這里也不是簡單的取出就完事了,后續(xù)還會對屬性進(jìn)行正則表達(dá)式的匹配,看與regexp
中的值是否能對的上,檢查無誤時才會返回這個tag
。
到這里,我們就解釋清楚了ScalarNode
中tag
屬性究竟是怎么獲取到的了,之后方法調(diào)用層層返回,返回到Origi
調(diào)用構(gòu)造器
在constructDocument
中,有兩步比較重要,第一步是推斷當(dāng)前節(jié)點(diǎn)應(yīng)該使用哪種類型的構(gòu)造器,第二步是使用獲得的構(gòu)造器來重新對Node
節(jié)點(diǎn)中的value
進(jìn)行賦值,簡易流程如下,省去了循環(huán)遍歷的部分:
nTrackingConstructor
父類BaseConstructor
的getData
方法中。接下來,繼續(xù)執(zhí)行constructDocument
方法,完成對yml文檔的解析。
推斷構(gòu)造器種類的過程也很簡單,在父類BaseConstructor
中,緩存了一個HashMap,存放了節(jié)點(diǎn)的tag
類型到對應(yīng)構(gòu)造器的映射關(guān)系。在getConstructor
方法中,就使用之前節(jié)點(diǎn)中存入的tag
屬性來獲得具體要使用的構(gòu)造器:
當(dāng)tag
為bool
類型時,會找到SafeConstruct
中的內(nèi)部類ConstructYamlBool
作為構(gòu)造器,并調(diào)用它的construct
方法實(shí)例化一個對象,來作為ScalarNode
節(jié)點(diǎn)的value
的值:
在construct
方法中,取到的val就是之前的on
,至于下面的這個BOOL_VALUES
,也是提前初始化好的一個HashMap,里面提前存放了一些對應(yīng)的映射關(guān)系,key是下面列出的這些關(guān)鍵字,value則是Boolean
類型的true
或false
:
到這里,yml中的屬性解析流程就基本完成了,我們也明白了為什么yml中的on
會被轉(zhuǎn)化為true
的原理了。
思考
那么,下一個問題來了,既然yml文件解析中會做這樣的特殊處理,那么如果換成properties
配置文件怎么樣呢?
sw.turnOn=on sw.turnOff=off
執(zhí)行一下程序,看一下結(jié)果:
可以看到,使用properties
配置文件能夠正常讀取結(jié)果,看來是在解析的過程中沒有做特殊處理,至于解析的過程,有興趣的小伙伴可以自己去閱讀一下源碼。
到此這篇關(guān)于SpringBoot解析yml全流程詳解的文章就介紹到這了,更多相關(guān)SpringBoot解析yml 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot使用mybatis-plus分頁查詢無效的問題解決
MyBatis-Plus提供了很多便捷的功能,包括分頁查詢,本文主要介紹了SpringBoot使用mybatis-plus分頁查詢無效的問題解決,具有一定的參考價值,感興趣的可以了解一下2023-12-12java如何實(shí)現(xiàn)項(xiàng)目啟動時執(zhí)行指定方法
這篇文章主要為大家詳細(xì)介紹了java項(xiàng)目如何啟動時執(zhí)行指定方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07SpringCloud Gateway的路由,過濾器和限流解讀
這篇文章主要介紹了SpringCloud Gateway的路由,過濾器和限流解讀,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02java單鏈表實(shí)現(xiàn)書籍管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java單鏈表實(shí)現(xiàn)書籍管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11