Apollo啟動(dòng)原理、配置拉取過(guò)程源碼分析

前言

apollo官方文檔: Apollo
本篇主要基于apollo-client 1.4.0源碼魁蒜,介紹springboot項(xiàng)目如何在啟動(dòng)時(shí)調(diào)用apollo景图,以及apollo客戶(hù)端遠(yuǎn)程獲取配置的流程蚯舱。 如有筆誤,歡迎評(píng)論留言交流指正逆趋。

一句話(huà)概括:apollo客戶(hù)端主要任務(wù),是在spring容器啟動(dòng)時(shí)晒奕,根據(jù)集群和namespace拉取對(duì)應(yīng)配置中心的配置數(shù)據(jù)闻书,再根據(jù)@Value注解注入對(duì)應(yīng)bean的對(duì)應(yīng)屬性,同時(shí)通過(guò)http請(qǐng)求監(jiān)聽(tīng)指定的集群和namespace對(duì)應(yīng)的配置是否發(fā)生變化吴汪,實(shí)現(xiàn)配置的動(dòng)態(tài)變化

1惠窄、SPI加載

找到META-INF路徑下的spring.factories文件,里面是SPI機(jī)制添加的初始化類(lèi)

首先項(xiàng)目啟動(dòng)時(shí)會(huì)調(diào)用initializer漾橙,這里是apollo初始化配置杆融、首次拉取配置、添加監(jiān)聽(tīng)器的入口霜运。其次是EnableAutoConfigration脾歇,這是自動(dòng)更新注入@value的關(guān)鍵

2、初始化方法

ApplicationContextInitializer 接口用于在 Spring 容器刷新之前執(zhí)行的一個(gè)回調(diào)函數(shù)淘捡,通常用于向 SpringBoot 容器中注入屬性藕各。通過(guò)實(shí)現(xiàn)ApplicationContextInitializer接口的initialize( )方法進(jìn)行初始化。

先看getManager( )方法焦除,直接說(shuō)結(jié)論激况,取到的是DefaultConfigManager類(lèi),接著分析一下ApolloInjector里的映射原理膘魄,為什么會(huì)取到這個(gè)類(lèi)
2.1 ApolloInjector初始化映射流程

首先打開(kāi)ApolloInjector類(lèi)乌逐,通過(guò)靜態(tài)方法構(gòu)造,雙重檢查加鎖 防止生成多個(gè)Injector创葡,調(diào)用loadFirst方法

在這里的提示已經(jīng)能猜出來(lái)浙踢,初始化是從/ META-INF/services/目錄下找Injector類(lèi)加載,再?gòu)拇a看看加載方法具體在哪里灿渴,進(jìn)入loadAll( ) 方法的iterator( )洛波,第一次進(jìn)入肯定是null 進(jìn)入構(gòu)造器

初始化時(shí)會(huì)創(chuàng)建兩個(gè)Iterator并調(diào)用他們的hasNext( )尋找胰舆、加載Injector

這樣就拿到了這個(gè)文件里寫(xiě)的DefaultInjector類(lèi),然后用反射調(diào)用DefaultInjector( )的構(gòu)造器

意味著ApolloInjector默認(rèn)加載的是DefaultInjector類(lèi)蹬挤,它在構(gòu)造器里會(huì)創(chuàng)建ApolloModule( )進(jìn)行組件初始化綁定

ApolloModule為什么能調(diào)用到configure( )就不展開(kāi)描述了缚窿,主要是用到了Guice依賴(lài)包的方法,不是apollo的闻伶。 這樣我們?cè)趇nitialize( )初始化方法里getManeger( )拿到的是DefaultConfigManager類(lèi) 再看getConfig( )方法滨攻,也是雙重校驗(yàn)加鎖

這里的getFactory一樣通過(guò)ApolloInjector拿的是DefaultConfigFactoryManager然后從DefaultConfigFactory調(diào)用create方法

第一個(gè)if是讀取本地配置 一般apollo讀取的都是遠(yuǎn)程配置,但是也可以開(kāi)啟讀取本地配置文件模式蓝翰,最后找到RemoteConfigRepository的構(gòu)造方法光绕,這個(gè)方法就是初始化拉取遠(yuǎn)程配置的關(guān)鍵

重點(diǎn)是下面三個(gè)方法
2.2 trySync( )方法

try Sync( )方法,里面嘗試http請(qǐng)求獲取配置文件中的遠(yuǎn)程服務(wù)地址

然后遍歷所有可以請(qǐng)求的集群地址畜份,傳遞appId诞帐、cluster、namespaces爆雹、releaseKey嘗試?yán)∨渲猛=叮瑀eleaseKey是版本號(hào),初始化值-1钙态,每次更新會(huì)放到本地緩存慧起,校驗(yàn)本地緩存和遠(yuǎn)程配置是否一致就是通過(guò)版本號(hào)。 拿到配置存入緩存和當(dāng)前配置進(jìn)行比較

存入緩存的操作主要是通過(guò)fireRepositoryChange( )方法調(diào)用監(jiān)聽(tīng)器册倒,最后會(huì)走到這里調(diào)用onChange方法蚓挤,對(duì)key-value變化進(jìn)行修改

此外這個(gè)注解@ApolloConfigChangeListener 可以指定對(duì)哪些namespaces監(jiān)聽(tīng)或者指定哪些keys監(jiān)聽(tīng),一般項(xiàng)目里可以默認(rèn)配置對(duì)所有事件變化都監(jiān)聽(tīng)驻子,并且 implements ApplicationContextAware灿意,拿到更新后的所有鍵值對(duì),對(duì)上下文里的bean進(jìn)行更新

不用這個(gè)@ApolloConfigChangeListener注解也可以手動(dòng)把當(dāng)前類(lèi)加入Listener崇呵,這樣啟動(dòng)或者配置變化也會(huì)進(jìn)入這個(gè)監(jiān)聽(tīng)器
2.3 另外兩個(gè)方法

schedulePeriodicRefresh( ) 線(xiàn)程池多線(xiàn)程執(zhí)行缤剧,定時(shí)5分鐘一次嘗試刷新配置信息

scheduleLongPollingRefresh( ) 通過(guò)http請(qǐng)求,長(zhǎng)輪詢(xún)查詢(xún)配置信息域慷,如果60秒沒(méi)有變化返回304狀態(tài)荒辕,如果配置有變化就返回200,調(diào)用sync( )方法進(jìn)行緩存更新

為什么是這樣的設(shè)計(jì)呢犹褒?網(wǎng)上找到其他博主請(qǐng)教了 Apollo 的作者宋老師兄纺。
原文指路:https://so.csdn.net/so/search?q=Apollo%20%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90%20%E2%80%94%E2%80%94%20Client%20%E8%BD%AE%E8%AF%A2%E9%85%8D%E7%BD%AE&t=blog&u=github_38592071

聊天內(nèi)容如下:

*   老艿艿:
    *   [https://github.com/ctripcorp/apollo/issues/652](https://github.com/ctripcorp/apollo/issues/652)
    *   看這個(gè) issue 問(wèn)了
    *   問(wèn)題:非常感謝。為什么不在 long polling 的返回結(jié)果中直接返回更新的結(jié)果呢化漆?

*   回答:這樣推送消息就是有狀態(tài)了,做不到冪等了钦奋,會(huì)帶來(lái)很多問(wèn)題座云。

*   [呲牙] 周末打擾哈疙赠。帶來(lái)的問(wèn)題主要是哪些哈?

*   張大佬:

    *   可能會(huì)有數(shù)據(jù)的丟失朦拖,如果使用只推送變更通知的話(huà)圃阳,即使丟失了,還是能在下一次變更的時(shí)候達(dá)到一個(gè)一致的狀態(tài)

*   宋老師:

    *   @老艿艿 主要是冪等考慮

    *   加載配置接口是冪等的璧帝,推送配置的話(huà)就做不到了捍岳,因?yàn)楹屯扑晚樞蛳嚓P(guān)

*   老艿艿:

    *   推送順序指的是?

*   張大佬:

    *   我想的是睬隶,比如兩次修改操作的通知锣夹,由于網(wǎng)絡(luò)問(wèn)題,客戶(hù)端接收到的順序可能是反的苏潜,如果這兩次修改的是同一個(gè)key银萍,就有問(wèn)題了。

*   老艿艿:

    *   從目前代碼看下來(lái)恤左,長(zhǎng)輪詢(xún)發(fā)起贴唇,同時(shí)只會(huì)有一個(gè),應(yīng)該不會(huì)同時(shí)兩個(gè)通知呀飞袋。

    *   現(xiàn)在 client 是一個(gè)長(zhǎng)輪詢(xún)+定時(shí)輪詢(xún)戳气,是不是這兩個(gè)會(huì)有互相的影響。

*   張大佬:

    *   @宋老師 嗯嗯

*   宋老師:

    *   目前推送是單連接走h(yuǎn)ttp的巧鸭,所以問(wèn)題可能不大瓶您,不過(guò)在設(shè)計(jì)上而言是有這個(gè)問(wèn)題的,比如如果推送是走的tcp長(zhǎng)連接的話(huà)蹄皱。另外览闰,長(zhǎng)輪詢(xún)和推送之間也會(huì)有沖突,如果連續(xù)兩次配置變化巷折,就可能造成雙寫(xiě)压鉴。還有一點(diǎn),就是保持邏輯的簡(jiǎn)單锻拘,目前的做法推送只負(fù)責(zé)做簡(jiǎn)單的通知油吭,不需要去計(jì)算客戶(hù)端的配置應(yīng)該是什么,因?yàn)橛?jì)算邏輯挺復(fù)雜的署拟,需要考慮集群婉宰,關(guān)聯(lián),灰度等推穷。

*   老艿艿:

    *   現(xiàn)在的設(shè)計(jì)思路上心包,是不是可以理解.

    *   1、client 的定時(shí)輪詢(xún)馒铃,可以保持最終一致蟹腾。

    *   2痕惋、client 的長(zhǎng)輪詢(xún),是定時(shí)輪訓(xùn)的“實(shí)時(shí)”補(bǔ)充娃殖,通過(guò)這樣的方式值戳,讓流程統(tǒng)一?

*   宋老師:

    *   總而言之炉爆,就是在滿(mǎn)足冪等性堕虹,實(shí)時(shí)性的基礎(chǔ)上保持設(shè)計(jì)的簡(jiǎn)單

    *   是的,推拉結(jié)合

*   張大佬:

    *   長(zhǎng)輪詢(xún)個(gè)推送直接的沖突沒(méi)太理解

    *   有沒(méi)有可能有這種問(wèn)題芬首,一次長(zhǎng)輪詢(xún)中消息丟失了桨菜,但是長(zhǎng)輪詢(xún)還在

*   老艿艿:  1\. 長(zhǎng)輪詢(xún)的通知里面浴捆,帶有配置信息  2\. 定時(shí)輪訓(xùn)箍铭,里面也拿到配置信息

    *   這個(gè)時(shí)候赁豆,client 是沒(méi)辦法判斷哪個(gè)配置信息是新的。

    *   @張大佬 我認(rèn)為是可能的艺晴,這個(gè)時(shí)候昼钻,client 可以發(fā)起新的長(zhǎng)輪詢(xún)。

*   宋大佬:

    *   @張大佬 長(zhǎng)輪詢(xún)和推送的沖突封寞,這個(gè)更正為定時(shí)輪詢(xún)和推送的沖突

*   老艿艿:

    *   通知是定時(shí)輪詢(xún)配置的補(bǔ)充然评。有了通知,立馬輪詢(xún)狈究。不用在定時(shí)了

*   張大佬:

    *   get

*   老艿艿:

    *   謝謝宋老師和張大佬[壞笑]  :::  三個(gè)獲取方法主要就是不停地查詢(xún)遠(yuǎn)程配置和本地的對(duì)比碗淌,不同就更新替換

3、自動(dòng)更新方法

接下來(lái)看EnableAutoConfigration類(lèi)如何實(shí)現(xiàn)自動(dòng)更新配置抖锥。 一句話(huà)概括:apollo通過(guò)這個(gè)類(lèi)和@Bean注解亿眠,向容器注入了多個(gè)加載器和監(jiān)聽(tīng)器,通過(guò)spring的其他注解在啟動(dòng)時(shí)拿到bean工廠(chǎng)和其中所有屬性磅废,最后通過(guò)監(jiān)聽(tīng)器不斷http長(zhǎng)輪詢(xún)監(jiān)聽(tīng)配置是否發(fā)生變化纳像,發(fā)生變化就通過(guò)java反射,將更新后的值注入bean的對(duì)應(yīng)屬性拯勉。

3.1 怎么做到自動(dòng)更新屬性

首先通過(guò)configuration注解和Bean注解竟趾,向容器注入了一個(gè)ConfigPropertySourcesProcessor類(lèi),它繼承了PropertySourcesProcessor類(lèi)宫峦,實(shí)現(xiàn)了postProcessBeanDefinitionRegistry類(lèi)

BeanDefinitionRegistryPostProcessor類(lèi)實(shí)現(xiàn)了postProcessBeanDefinitionRegistry( )方法岔帽,用于啟動(dòng)時(shí)構(gòu)造幾個(gè)必要的后置處理器,SpringValueProcessor后面會(huì)講到导绷,ApolloAnnotationProcessor用來(lái)掃描@ApolloConfigChangeListener注解犀勒,然后執(zhí)行addLinstener操作

PropertySourcesProcessor實(shí)現(xiàn)了這個(gè)接口,創(chuàng)建了自動(dòng)更新的監(jiān)聽(tīng)器,實(shí)現(xiàn)這個(gè)接口可以獲取到spring容器中所有的bean贾费,但沒(méi)有被初始化枚碗,可以被修改屬性值

可以看到,它遍歷了每個(gè)ConfigPropertySource铸本,對(duì)應(yīng)每一個(gè)namespaces,都加上了同一個(gè)自動(dòng)更新監(jiān)聽(tīng)器

在這個(gè)AutoUpdateConfigChangeListener中遵堵,由于他也實(shí)現(xiàn)了ConfigChangeListener類(lèi)箱玷,依然是用onChange( )方法更新,方法里用springValueRegistry和beanFactory根據(jù)key查找所有涉及到的地方陌宿,然后自動(dòng)實(shí)現(xiàn)更新具體的值

在監(jiān)聽(tīng)器里锡足,拿到spring的BeanFactory和監(jiān)聽(tīng)到更新的key,再找到哪個(gè)具體的類(lèi)壳坪,哪個(gè)具體的屬性值需要更新舶得,如上圖,在update( )方法里還是用java反射重新注入新的值爽蝴。

3.2 為什么@Value的值會(huì)被更新

關(guān)于springValueRegistry里為什么有那些屬性沐批、怎么獲取的@Value的值,在最初注入的加載器中蝎亚,這個(gè)加載器根據(jù)所有的bean和屬性名九孩,維護(hù)了一個(gè)存儲(chǔ)所有@Value注解所在位置的關(guān)系數(shù)據(jù)結(jié)構(gòu)

可以看到,這個(gè)springValueRegistry實(shí)現(xiàn)了BeanPostProcessor接口发框,用postProcessBeforeInitialization( )方法躺彬,在啟動(dòng)之前掃描所有的@Value的值,之后都會(huì)調(diào)用register( )注冊(cè)方法梅惯,把對(duì)應(yīng)的值注冊(cè)到springValueRegistry里面宪拥,而springValue類(lèi)中保存著key、value铣减、bean她君、beanName的值,方便后續(xù)更新注入時(shí)找到對(duì)應(yīng)位置

總結(jié)

apollo客戶(hù)端通過(guò)注解在spring啟動(dòng)時(shí)拿到所有的bean徙歼,通過(guò)自己的XXXProcessor類(lèi)犁河,掃描@Value注解和@ApolloConfigChangeListener注解。前者跟bean一起封裝存儲(chǔ)到本地的內(nèi)存中魄梯,方便后續(xù)自動(dòng)更新時(shí)找到和注入對(duì)應(yīng)的屬性桨螺,后者允許開(kāi)發(fā)者拓展自己的監(jiān)聽(tīng)器,在更新時(shí)執(zhí)行額外操作酿秸,或是對(duì)指定的變化進(jìn)行處理灭翔。apollo對(duì)屬性的注入是通過(guò)java反射機(jī)制,對(duì)配置變化的監(jiān)聽(tīng)是通過(guò)http請(qǐng)求,同時(shí)用SPI機(jī)制給開(kāi)發(fā)者留了非常多的拓展空間肝箱。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末哄褒,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子煌张,更是在濱河造成了極大的恐慌呐赡,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件骏融,死亡現(xiàn)場(chǎng)離奇詭異链嘀,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)档玻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)怀泊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人误趴,你說(shuō)我怎么就攤上這事霹琼。” “怎么了凉当?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵枣申,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我纤怒,道長(zhǎng)糯而,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任泊窘,我火速辦了婚禮熄驼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘烘豹。我一直安慰自己瓜贾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布携悯。 她就那樣靜靜地躺著祭芦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪憔鬼。 梳的紋絲不亂的頭發(fā)上龟劲,一...
    開(kāi)封第一講書(shū)人閱讀 49,792評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音轴或,去河邊找鬼昌跌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛照雁,可吹牛的內(nèi)容都是我干的蚕愤。 我是一名探鬼主播,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼萍诱!你這毒婦竟也來(lái)了悬嗓?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤裕坊,失蹤者是張志新(化名)和其女友劉穎包竹,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體籍凝,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡映企,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了静浴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡挤渐,死狀恐怖苹享,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情浴麻,我是刑警寧澤得问,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站软免,受9級(jí)特大地震影響宫纬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜膏萧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一漓骚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧榛泛,春花似錦蝌蹂、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至沛简,卻和暖如春齐鲤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背椒楣。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工给郊, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人撒顿。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓丑罪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子吩屹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容