8000字+22張圖探秘SpringCloud配置中心的核心原理
大家好,我是三友~~
這篇文章來扒一扒SpringCloud配置中心的核心原理。
不知你是否跟我一樣,在剛開始使用SpringCloud配置中心的時候也有很多的疑惑:
- SpringCloud是什么時候去拉取配置中心的?
- 配置中心客戶端的配置信息為什么要寫在bootstrap文件中?
- 對象中注入的屬性是如何動態(tài)刷新的?
- 一些開源的配置中心是如何整合SpringCloud的?
- ...
本文就通過探討上述問題來探秘SpringCloud配置中心核心的底層原理。
從SpringBoot的啟動過程說起
在SpringBoot啟動的時候會經(jīng)歷一系列步驟,核心就是SpringApplication的run方法的邏輯
圖片
整個過程大致可以劃分為三個階段:
圖片
ApplicationContext刷新前階段,這個階段主要也干三件事
- 準(zhǔn)備Environment(注意我這里加粗了,你懂得),也就是準(zhǔn)備SpringBoot的整個外部化配置的對象
- 創(chuàng)建一個ApplicationContext
- 為ApplicationContext做一些準(zhǔn)備工作
圖片
ApplicationContext刷新階段,這個階段其實(shí)就是調(diào)用ApplicationContext#refresh方法來刷新容器
圖片
刷新的整個過程可以看我之前寫的萬字+20張圖剖析Spring啟動時12個核心步驟這篇文章
ApplicationContext刷新后階段,這個階段其實(shí)就是收尾的階段,這個過程其實(shí)沒有什么非常核心的事
ok,在說完上面這三個階段之后,思考一個問題
你覺得在上面的三個階段,哪個階段最有可能從配置中心拉取配置?
其實(shí)稍微思考一下,肯定是想到的就是刷新前階段
因?yàn)槲乙呀?jīng)明示了,準(zhǔn)備Environment
玩笑歸玩笑,為什么是這個階段?
很好理解,因?yàn)檫@個階段是準(zhǔn)備Environment,也就是準(zhǔn)備外部化配置
只需要在這個階段加載配置中心的配置,放到Environment中,后面在整個ApplicationContext刷新階段創(chuàng)建Bean的時候,就可以使用到配置中心的配置了
其實(shí)不光是配置中心的配置,比如配置文件的配置,也是在這里階段讀取的
至于如何實(shí)現(xiàn)的,我們接著往下瞅
準(zhǔn)備Environment的核心操作
上一節(jié)得出一個結(jié)論
準(zhǔn)備Environment,也就是prepareEnvironment方法的實(shí)現(xiàn),是拉取配置的核心
prepareEnvironment方法
不過在說這個方法之前,先來講一下一些前置操作
前置操作
在SpringApplication創(chuàng)建的時候,會去加載spring.factories中的一些對象,其中就包括:
- org.springframework.context.ApplicationListener鍵對應(yīng)的ApplicationListener的實(shí)現(xiàn)
- org.springframework.boot.SpringApplicationRunListener鍵對應(yīng)的SpringApplicationRunListener的實(shí)現(xiàn)類
SpringApplicationRunListener僅僅只有一個實(shí)現(xiàn)EventPublishingRunListener
構(gòu)造的時候會創(chuàng)建一個SimpleApplicationEventMulticaster,再將加載的ApplicationListener添加進(jìn)去
SimpleApplicationEventMulticaster是用來發(fā)布事件用的,不清楚的話可以看三萬字盤點(diǎn)Spring 9大核心基礎(chǔ)功能這篇文章
按照傳統(tǒng),畫張圖來理一下這部分前置操作
圖片
prepareEnvironment的核心邏輯
接著來講一下prepareEnvironment方法
prepareEnvironment方法
這個方法會首先創(chuàng)建一個Environment對象
之后會執(zhí)行這么一行方法,傳入剛剛創(chuàng)建的Environment對象
listeners.environmentPrepared(environment);
EventPublishingRunListener#environmentPrepared
圖片
這個方法最終會發(fā)布一個ApplicationEnvironmentPreparedEvent事件
而對這個事件有兩個特別重要的監(jiān)聽器:
- ConfigFileApplicationListener
- BootstrapApplicationListener
這些監(jiān)聽器都是通過前置操作從spring.factories配置文件中加載的
ConfigFileApplicationListener,用來處理配置文件的,他會解析配置文件的配置,放到Environment中
BootstrapApplicationListener這個跟本文探討的主題相關(guān)了,它是用來專門來跟配置中心交互的
到這,我們就找到了SpringCloud配置中心配置拉取的整個入口邏輯
不過在分析BootstrapApplicationListener是如何從配置中心拉取配置的之前,先來張圖總結(jié)一下這部分prepareEnvironment的操作
圖片
SpringCloud是如何巧妙地拉取配置的?
在BootstrapApplicationListener中,他首先也會創(chuàng)建一個SpringApplication去執(zhí)行
圖片
其實(shí)本質(zhì)上就是創(chuàng)建一個Spring容器,也就是ApplicationContext
這個容器非常重要,這個容器是專門用來跟配置中心交互的
這個容器在創(chuàng)建的時候會給它兩個比較重要的配置
第一個就是設(shè)置這個容器所用的配置文件的名稱
圖片
默認(rèn)就是bootstrap
這就解釋了為什么配置中心的配置信息需要寫在bootstrap配置文件中
第二個就是會加入一個配置類
BootstrapImportSelectorConfiguration
圖片
這個配置類又會通過@Import注解導(dǎo)入另一個配置類
BootstrapImportSelector
圖片
BootstrapImportSelector實(shí)現(xiàn)了(間接)ImportSelector接口
那么這個容器在啟動的時候,就會調(diào)用BootstrapImportSelector的selectImports方法的實(shí)現(xiàn)獲取到一些配置類
而BootstrapImportSelector的selectImports實(shí)現(xiàn)從截圖中也就可以看出
他會加載所有的spring.factories中的鍵為org.springframework.cloud.bootstrap.BootstrapConfiguration的配置類
其實(shí)這里@BootstrapConfiguration的作用其實(shí)跟@EnableAutoConfiguration的作用是差不多的,都是用來導(dǎo)入配置類的
所以,總的來說,這個用來跟配置中心交互的Spring容器最最主要就是干兩件事:
- 加載bootstrap配置文件
- 加載所有的spring.factories中的鍵為org.springframework.cloud.bootstrap.BootstrapConfiguration對應(yīng)的配置類
圖片
而在spring-cloud-context包下,@BootstrapConfiguration會導(dǎo)入一個很重要的配置類
圖片
PropertySourceBootstrapConfiguration
圖片
PropertySourceBootstrapConfiguration
這個配置類中會注入這么一個集合對象
PropertySourceLocator
PropertySourceLocator
這個接口非常非常重要,先來看看注釋
Strategy for locating (possibly remote) property sources for the Environment. Implementations should not fail unless they intend to prevent the application from starting.
我用我的四級英語功力給大家翻譯一下
以一種策略的方式為Environment定位(可能是遠(yuǎn)程)屬性配置(PropertySource)。實(shí)現(xiàn)不應(yīng)該失敗,除非打算阻止應(yīng)用程序啟動。
從這個翻譯后的意思就是說,這個接口是用來定位,也就是說獲取屬性配置的
并且可能是遠(yuǎn)程告訴我們一個很重要的信息,那就是獲取的配置信息不僅僅可以存在本地,而且還可以存在遠(yuǎn)程。
遠(yuǎn)程?作者這里就差直接告訴你可以從配置中心獲取了。。
所以這個接口的作用就是用配置中心獲取配置的!
那么自然而然不同的配置中心要想整合到SpringCloud就得實(shí)現(xiàn)這個接口
當(dāng)注入完P(guān)ropertySourceLocator集合之后,在某個階段會調(diào)用所有的PropertySourceLocator,獲取配置中心中的配置
之后在把這些配置放到Environment中
這樣在ApplicationContext的刷新階段就可以使用到配置中心的那些配置了
小總結(jié)
到這我們就弄明白了在項(xiàng)目啟動中加載配置中心的配置了
其實(shí)就是項(xiàng)目在啟動時會額外創(chuàng)建一個跟配置中心相關(guān)的Spring容器
這個容器會去加載bootstrap配置文件和所有的spring.factories中的鍵為org.springframework.cloud.bootstrap.BootstrapConfiguration對應(yīng)的配置類
之后會去調(diào)用這個容器中所有的PropertySourceLocator對象,從配置中心獲取配置
再放到Environment中就完成了啟動時從配置中心獲取配置的方式
最后,來張全家福概括一下前面整體的步驟
圖片
如何動態(tài)刷新Bean的屬性?
我們都知道,要想實(shí)現(xiàn)配置屬性的動態(tài)刷新,需要在類上加上一個注解
@RefreshScope
圖片
重點(diǎn)來了
加了@RefreshScope注解的Bean,就拿上圖中的UserService舉例
Spring在生成的時候會生成兩個UserService的Bean:
- 第一個是UserService的代理動態(tài)代理的Bean,后面我稱為第一個Bean
- 第二個就是UserService這個Bean,后面我稱為第二個Bean
當(dāng)你在其它類中需要注入一個UserService時,真正注入的是第一個Bean,也就是動態(tài)代理的Bean
當(dāng)你使用這個注入的動態(tài)代理的Bean的時候,他會去找第二個Bean,也就是真正的UserService這個Bean,然后調(diào)用對應(yīng)的方法
圖片
比如你調(diào)用注入的UserService代理對象的getUsername方法,最終就會調(diào)用到第二個BeangetUsername方法
獲取到的username屬性值自然也就是第二個Bean中的username值
那么為什么要生成兩個Bean?
接著往下瞅
在SpringCloud中有這么一項(xiàng)規(guī)定
當(dāng)配置中心客戶端一旦感知到服務(wù)端的某個配置有變化的時候,需要發(fā)布一個RefreshEvent事件來告訴SpringCloud配置有變動
圖片
在SpringCloud中RefreshEventListener類會去監(jiān)聽這個事件
RefreshEventListener
一旦監(jiān)聽到這個事件,SpringCloud會再次從配置中心拉取配置
這個拉取配置的核心邏輯跟啟動時拉取配置的核心邏輯是一樣的
也是通過 BootstrapApplicationListener 來實(shí)現(xiàn)的
圖片
這部分代碼邏輯在ContextRefresher類中,順著RefreshEventListener就能看到,有興趣可以扒一扒
怕你忘了,我再把上面拉取配置的圖拿過來
圖片
有了最新的配置之后,就會進(jìn)行一步騷操作來移花接木”刷新“注入到對象的屬性
這個騷操作就是銷毀所有的前面提到的第二個Bean,但是第一個Bean,也就是代理對象保持不變
圖片
當(dāng)程序運(yùn)行調(diào)用代理對象的方法的時候,發(fā)現(xiàn)第二個Bean沒有了,此時他就會去重新創(chuàng)建第二個Bean,也就是重新創(chuàng)建一個UserService對象
由于此時已經(jīng)拉到最新的配置了,也就是這個被重新創(chuàng)建的UserService對象注入的就是最新的屬性了
圖片
之后再調(diào)用的這個新創(chuàng)建的第二個Bean,拿到的自然就是最新的配置
所以,給你的感覺是對象的屬性發(fā)生了變化,實(shí)際上是真正被調(diào)用的對象重新創(chuàng)建了
所以這招移花接木還是有點(diǎn)意思的!
小總結(jié)
其實(shí)到這就弄明白了Bean的屬性動態(tài)刷新的原理
其實(shí)就是當(dāng)配置中心客戶端發(fā)現(xiàn)服務(wù)端的配置有變化,需要發(fā)送一個RefreshEvent事件來告訴SpringCloud配置有變動
SpringCloud會去監(jiān)聽這個事件,按照項(xiàng)目啟動的方式重新拉取配置中心最新的屬性配置
當(dāng)拉取完屬性配置之后,就會銷毀所有的第二個Bean,也就是真正被使用的Bean
之后當(dāng)?shù)谝粋€Bean(動態(tài)代理的Bean)需要使用這個第二個Bean時,就會重新創(chuàng)建這個第二個Bean
此時由于已經(jīng)有最新的配置了,那么創(chuàng)建的這個第二個Bean就會被注入最新的屬性,這樣就實(shí)現(xiàn)了屬性的”刷新“
圖片
補(bǔ)充個東西:@RefreshScope的秘密
上面大致說了@RefreshScope動態(tài)刷新的原理
這里我補(bǔ)充一下@RefreshScope代碼層面的實(shí)現(xiàn)原理
本來這部分原理我是寫在前面的,但是我發(fā)現(xiàn)這塊比較繞,怕打斷文章的節(jié)奏,所以就準(zhǔn)備刪除了
但是想想既然都寫了,那么就給放到補(bǔ)充里面吧,看不懂也不耽誤前面的理解
圖片
這個注解是個衍生注解,真正起作用的就是@Scope注解
@Scope注解并不陌生,他其實(shí)是定義Bean的作用域
比如多例(原型),就可以加上@Scope("prototype")注解
還有一些八股文常背的作用域,比如session作用域等等
而@RefreshScope也可以看做是一種Bean的作用域,名字叫做refresh
這些除了單例和多例之外的作用域的底層實(shí)現(xiàn)邏輯都是一樣的
這些帶有作用域的Bean相比于普通的單例Bean主要有以下幾點(diǎn)不同:
- 會注冊兩個Bean,這個前面已經(jīng)提到過
- 保存的地方不同,比如單例Bean最終會存在三級緩存中的第一級緩存中,而不同作用域的Bean是存在不同的地方的
先說會注冊兩個Bean,還是以前面提到的UserService舉個例子,這兩個Bean分別是
- 第一個Bean的Bean名稱為userService,Bean class為ScopedProxyFactoryBean.class,這個scope為默認(rèn),也就是單例
- 第二個Bean的Bean名稱為scopedTarget.userService,Bean class為UserService.class,scope為refresh(如果是session作用域就是session)
第一個Bean的class為ScopedProxyFactoryBean,是個FactoryBean的實(shí)現(xiàn)
圖片
這個最終會生成一個代理對象,上面的例子就是為UserService生成一個代理對象,并且由于是單例的,所以最終這個對象會被放到一級緩存中,我們使用時注入的也就是這個對象
第二個Bean的class是UserService,所以生成的就是真正的UserService對象,但是由于scope為refresh,所以不會存在第一級緩存中
這部分注冊兩個Bean的代碼是在ScopedProxyUtils#createScopedProxy方法中,有興趣的可以扒扒
再來講一講保存的地方不同
不同的作用域都需要實(shí)現(xiàn)一個Scope接口來存放對應(yīng)的Bean
圖片
比如refresh、session作用域都有對應(yīng)的實(shí)現(xiàn)
圖片
也就是通過Scope就可以管理不同作用域的Bean
所以,對于refresh這個作用域來說,他的所有的Bean都在RefreshScope中
后面說的銷毀,只需要移除RefreshScope中的Bean就可以了
圖片
代碼也在ContextRefresher類中
開源配置中心是如何整合SpringCloud的?
首先我們再來梳理一下拉取配置和刷新配置的核心關(guān)鍵點(diǎn)
拉取配置關(guān)鍵點(diǎn)就是項(xiàng)目啟動的時候(也包括重新拉取配置),會去創(chuàng)建一個容器
這個容器只讀取bootstrap配置文件和spring.factories中的鍵為org.springframework.cloud.bootstrap.BootstrapConfiguration對應(yīng)的配置類
之后會獲取這個容器中的PropertySourceLocator,從而獲取配置中心的配置
刷新配置關(guān)鍵點(diǎn)就是一旦配置中心配置變動,就需要發(fā)送RefreshEvent事件,之后一系列刷新操作都是由SpringCloud的來完成的
所以,配置中心整合到SpringCloud其實(shí)就很簡單,就兩點(diǎn)
第一點(diǎn)就是需要實(shí)現(xiàn)PropertySourceLocator,并且配置中心一些相關(guān)的Bean需要通過org.springframework.cloud.bootstrap.BootstrapConfiguration來裝配到這個容器中
第二點(diǎn),當(dāng)配置發(fā)生變更需要發(fā)送RefreshEvent事件,這部分配置中心一些相關(guān)的Bean配置肯定是需要通過自動裝配來完成
有了這兩點(diǎn)我們來看看Nacos作為配置中心是如何整合到SpringCloud的
我們直接看Nacos的spring.factories文件
圖片
NacosConfigBootstrapConfiguration是用來實(shí)現(xiàn)第一點(diǎn)的
圖片
除了Nacos自己的一些Bean,他還聲明了一個NacosPropertySourceLocator這個Bean
NacosPropertySourceLocator
這個Bean就實(shí)現(xiàn)了PropertySourceLocator接口
第二點(diǎn)的實(shí)現(xiàn)就是通過NacosConfigAutoConfiguration配置類來實(shí)現(xiàn)的
這里面有這么一個Bean
圖片
這個Bean就實(shí)現(xiàn)了配置變化發(fā)送事件的操作
圖片
除了Nacos,比如說Consul作為配置中心的時候也是這么一套實(shí)現(xiàn)邏輯
但是值的注意的是,像Apollo配置中心,他并沒有適配SpringCloud這套規(guī)范
當(dāng)然,如果你有興趣,可以自己實(shí)現(xiàn)Apollo適配SpringCloud這套規(guī)范