女朋友問我:Dubbo的服務(wù)引用過程
本文轉(zhuǎn)載自微信公眾號「三太子敖丙」,作者三太子敖丙 。轉(zhuǎn)載本文請聯(lián)系三太子敖丙公眾號。
這篇文章我就帶著大家再來看看 Dubbo 服務(wù)引入全流程,這篇服務(wù)引入寫完下一篇就要來個(gè)全鏈路打通了,看看大家看完會不會有種任督二脈都被打通的感覺。
在寫文章的過程中丙還發(fā)現(xiàn)官網(wǎng)的一點(diǎn)小問題,下文中會提到。
話不多說,咱們直接進(jìn)入正題。
服務(wù)引用大致流程
我們已經(jīng)得知 Provider將自己的服務(wù)暴露出來,注冊到注冊中心,而 Consumer無非就是通過一波操作從注冊中心得知 Provider 的信息,然后自己封裝一個(gè)調(diào)用類和 Provider 進(jìn)行深入地交流。
而之前的文章我都已經(jīng)提到在 Dubbo中一個(gè)可執(zhí)行體就是 Invoker,所有調(diào)用都要向 Invoker 靠攏,因此可以推斷出應(yīng)該要先生成一個(gè) Invoker,然后又因?yàn)榭蚣苄枰磺秩霕I(yè)務(wù)代碼的方向發(fā)展,那我們的 Consumer 需要無感知的調(diào)用遠(yuǎn)程接口,因此需要搞個(gè)代理類,包裝一下屏蔽底層的細(xì)節(jié)。
整體大致流程如下:
服務(wù)引入的時(shí)機(jī)服務(wù)的引入和服務(wù)的暴露一樣,也是通過 spring 自定義標(biāo)簽機(jī)制解析生成對應(yīng)的 Bean,Provider Service 對應(yīng)解析的是 ServiceBean 而 Consumer Reference 對應(yīng)的是 ReferenceBean。
前面服務(wù)暴露的時(shí)機(jī)我們上篇文章分析過了,在 Spring 容器刷新完成之后開始暴露,而服務(wù)的引入時(shí)機(jī)有兩種,第一種是餓漢式,第二種是懶漢式。
餓漢式是通過實(shí)現(xiàn) Spring 的InitializingBean接口中的 afterPropertiesSet方法,容器通過調(diào)用 ReferenceBean的 afterPropertiesSet方法時(shí)引入服務(wù)。
懶漢式是只有當(dāng)這個(gè)服務(wù)被注入到其他類中時(shí)啟動(dòng)引入流程,也就是說用到了才會開始服務(wù)引入。
默認(rèn)情況下,Dubbo 使用懶漢式引入服務(wù),如果需要使用餓漢式,可通過配置 dubbo:reference 的 init 屬性開啟。
我們可以看到 ReferenceBean還實(shí)現(xiàn)了FactoryBean接口,這里有個(gè)關(guān)于 Spring 的面試點(diǎn)我?guī)Т蠹曳治鲆徊ā?/p>
BeanFactory 、FactoryBean、ObjectFactory
就是這三個(gè)玩意,我單獨(dú)拿出來說一下,從字面上來看其實(shí)可以得知BeanFactory、ObjectFactory是個(gè)工廠而FactoryBean是個(gè) Bean。
BeanFactory 其實(shí)就是 IOC 容器,有多種實(shí)現(xiàn)類我就不分析了,簡單的說就是 Spring 里面的 Bean 都?xì)w它管,而FactoryBean也是 Bean 所以說也是歸 BeanFactory 管理的。
那 FactoryBean 到底是個(gè)什么 Bean 呢?它其實(shí)就是把你真實(shí)想要的 Bean 封裝了一層,在真正要獲取這個(gè) Bean 的時(shí)候容器會調(diào)用 FactoryBean#getObject() 方法,而在這個(gè)方法里面你可以進(jìn)行一些復(fù)雜的組裝操作。
這個(gè)方法就封裝了真實(shí)想要的對象復(fù)雜的創(chuàng)建過程。
到這里其實(shí)就很清楚了,就是在真實(shí)想要的 Bean 創(chuàng)建比較復(fù)雜的情況下,或者是一些第三方 Bean 難以修改的情形,使用 FactoryBean 封裝了一層,屏蔽了底層創(chuàng)建的細(xì)節(jié),便于 Bean 的使用。
而 ObjectFactory 這個(gè)是用于延遲查找的場景,它就是一個(gè)普通工廠,當(dāng)?shù)玫?ObjectFactory 對象時(shí),相當(dāng)于 Bean 沒有被創(chuàng)建,只有當(dāng) getObject() 方法時(shí),才會觸發(fā) Bean 實(shí)例化等生命周期。
主要用于暫時(shí)性地獲取某個(gè) Bean Holder 對象,如果過早的加載,可能會引起一些意外的情況,比如當(dāng) Bean A 依賴 Bean B 時(shí),如果過早地初始化 A,那么 B 里面的狀態(tài)可能是中間狀態(tài),這時(shí)候使用 A 容易導(dǎo)致一些錯(cuò)誤。
總結(jié)的說 BeanFactory 就是 IOC 容器,F(xiàn)actoryBean 是特殊的 Bean, 用來封裝創(chuàng)建比較復(fù)雜的對象,而 ObjectFactory 主要用于延遲查找的場景,延遲實(shí)例化對象。
服務(wù)引入的三種方式
服務(wù)的引入又分為了三種,第一種是本地引入、第二種是直接連接引入遠(yuǎn)程服務(wù)、第三種是通過注冊中心引入遠(yuǎn)程服務(wù)。
本地引入不知道大家是否還有印象,之前服務(wù)暴露的流程每個(gè)服務(wù)都會通過搞一個(gè)本地暴露,走 injvm 協(xié)議(當(dāng)然你要是 scope = remote 就沒本地引用了),因?yàn)榇嬖谝粋€(gè)服務(wù)端既是 Provider 又是 Consumer 的情況,然后有可能自己會調(diào)用自己的服務(wù),因此就弄了一個(gè)本地引入,這樣就避免了遠(yuǎn)程網(wǎng)絡(luò)調(diào)用的開銷。
所以服務(wù)引入會先去本地緩存找找看有沒有本地服務(wù)。
直連遠(yuǎn)程引入服務(wù),這個(gè)其實(shí)就是平日測試的情況下用用,不需要啟動(dòng)注冊中心,由 Consumer 直接配置寫死 Provider 的地址,然后直連即可。
注冊中心引入遠(yuǎn)程服務(wù),這個(gè)就是重點(diǎn)了,Consumer 通過注冊中心得知 Provider 的相關(guān)信息,然后進(jìn)行服務(wù)的引入,這里還包括多注冊中心,同一個(gè)服務(wù)多個(gè)提供者的情況,如何抉擇如何封裝,如何進(jìn)行負(fù)載均衡、容錯(cuò)并且讓使用者無感知,這就是個(gè)技術(shù)活。
本文用的就是單注冊中心引入遠(yuǎn)程服務(wù),讓我們來看看 Dubbo 是如何做的吧。
服務(wù)引入流程解析
默認(rèn)是懶漢式的,所以服務(wù)引入的入口就是 ReferenceBean 的 getObject 方法。
可以看到很簡單,就是調(diào)用 get 方法,如果當(dāng)前還沒有這個(gè)引用那么就執(zhí)行 init 方法。
官網(wǎng)的一個(gè)小問題這個(gè)問題
就在 if (ref == null) 這一行,其實(shí)是一位老哥在調(diào)試的時(shí)候發(fā)現(xiàn)這個(gè) ref 竟然不等于 null,因此就進(jìn)不到 init 方法里面調(diào)試了,后來他發(fā)現(xiàn)是因?yàn)?IDEA 為了顯示對象的信息,會通過 toString 方法獲取對象對應(yīng)的信息。
toString 調(diào)用的是 AbstractConfig#toString,而這個(gè)方法會通過反射調(diào)用了 ReferenceBean 的 getObject 方法,觸發(fā)了引入服務(wù)動(dòng)作,所以說到斷點(diǎn)的時(shí)候 ref != null。

可以看到是通過方法名來進(jìn)行反射調(diào)用的,而 getObject 就是 get 開頭的,因此會被調(diào)用。
所以這個(gè)哥們提了個(gè) PR,但是一開始沒有被接受,一位 Member 認(rèn)為這不是 bug, idea 設(shè)置一下不讓調(diào)用 toString 就好了。
不過另一位 Member 覺得這個(gè) PR 挺好的,并且 Dubbo 項(xiàng)目二代掌門人北緯30也發(fā)話了,因此這個(gè) PR 被受理了。
至此我們已經(jīng)知道這個(gè)小問題了,然后官網(wǎng)上其實(shí)也寫的很清楚。
但是小問題來了,之前我在文章提到我的源碼版本是 2.6.5,是在 github 的 releases 里面下的,這個(gè) tostring 問題其實(shí)我挺早之前就知道了,我想的是我 2.6.5 穩(wěn)的一批,誰知道翻車了。
我調(diào)試的時(shí)候也沒進(jìn)到 init 方法因?yàn)?ref 也沒等于 null,我就奇怪了,我里面去看了下 toString 方法,2.6.5版本竟然沒有修改?沒有將 getObject 做過濾,因此還是被調(diào)用了。
我又打開了2.7.5版本的代碼,發(fā)現(xiàn)是修改過的判斷。
我又去特意下了 2.6.6 版本的代碼,發(fā)現(xiàn)也是修改過的,因此這個(gè)修改并不是隨著 2.6.5版本發(fā)布,而是 2.6.6,除非我下的是個(gè)假包,這就是我說的小問題了,不過影響不大。
其實(shí)提到這一段主要想說的是那個(gè) PR,作為一個(gè)開源軟件的輸出者,很多細(xì)節(jié)也是很重要的,這個(gè)問題其實(shí)很影響源碼的調(diào)試,因?yàn)閷Υa不熟,肯定會一臉懵逼,誰知道是不是哪個(gè)后臺線程異步引入了呢。
提這個(gè) PR 的老哥花了兩個(gè)小時(shí)才搞清楚真正的原因,所以說雖然這不是個(gè) bug 但是很影響那些想深入了解 Dubbo 內(nèi)部結(jié)構(gòu)的同學(xué)們,這種改配置去適應(yīng)的方案是不可取了,還好最終的方案是改代碼。
好了讓我們回到今天的主題,接下來分析的就是那個(gè)不讓我進(jìn)去的 init 方法了。
源碼分析
init 方法很長,不過大部分就是檢查配置然后將配置構(gòu)建成 map ,這一大段我就不分析了,我們直接看一下構(gòu)建完的 map 長什么樣。
然后就進(jìn)入重點(diǎn)方法 createProxy,從名字可以得到就是要?jiǎng)?chuàng)建的一個(gè)代理,因?yàn)榇a很長,我就一段一段的分析。
如果是走本地的話,那么直接構(gòu)建個(gè)走本地協(xié)議的 URL 然后進(jìn)行服務(wù)的引入,即 refprotocol.refer,這個(gè)方法之后會做分析,本地的引入就不深入了,就是去之前服務(wù)暴露的 exporterMap 拿到服務(wù)。
如果不是本地,那肯定是遠(yuǎn)程了,接下來就是判斷是點(diǎn)對點(diǎn)直連 provider 還是通過注冊中心拿到 provider 信息再連接 provider 了,我們分析一下配置了 url 的情況,如果配置了 url 那么不是直連的地址,就是注冊中心的地址。
然后就是沒配置 url 的情況,到這里肯定走的就是注冊中心引入遠(yuǎn)程服務(wù)了。
最終拼接出來的 URL 長這樣。
可以看到這一部分其實(shí)就是根據(jù)各種參數(shù)來組裝 URL ,因?yàn)槲覀兊淖赃m應(yīng)擴(kuò)展都需要根據(jù) URL 的參數(shù)來進(jìn)行的。
至此我先畫個(gè)圖,給大家先捋一下。
這其實(shí)就是整個(gè)流程了,簡述一下就是先檢查配置,通過配置構(gòu)建一個(gè) map ,然后利用 map 來構(gòu)建 URL ,再通過 URL 上的協(xié)議利用自適應(yīng)擴(kuò)展機(jī)制調(diào)用對應(yīng)的 protocol.refer 得到相應(yīng)的 invoker 。
在有多個(gè) URL 的時(shí)候,先遍歷構(gòu)建出 invoker 然后再由 StaticDirectory 封裝一下,然后通過 cluster 進(jìn)行合并,只暴露出一個(gè) invoker 。
然后再構(gòu)建代理,封裝 invoker 返回服務(wù)引用,之后 Comsumer 調(diào)用的就是這個(gè)代理類。
相信通過圖和上面總結(jié)性的簡述已經(jīng)知道大致的服務(wù)引入流程了,不過還是有很多細(xì)節(jié),比如如何從注冊中心得到 Provider 的地址,invoker 里面到底是怎么樣的?別急,我們繼續(xù)看。
從前面的截圖我們可以看到此時(shí)的協(xié)議是 registry 因此走的是 RegistryProtocol#refer,我們來看一下這個(gè)方法。
主要就是獲取注冊中心實(shí)例,然后調(diào)用 doRefer 進(jìn)行真正的 refer。
這個(gè)方法很關(guān)鍵,可以看到生成了RegistryDirectory 這個(gè) directory 塞了注冊中心實(shí)例,它自身也實(shí)現(xiàn)了NotifyListener 接口,因此注冊中心的監(jiān)聽其實(shí)是靠這家伙來處理的。
然后向注冊中心注冊自身的信息,并且向注冊中心訂閱了 providers 節(jié)點(diǎn)、 configurators 節(jié)點(diǎn) 和 routers 節(jié)點(diǎn),訂閱了之后 RegistryDirectory 會收到這幾個(gè)節(jié)點(diǎn)下的信息,就會觸發(fā) DubboInvoker 的生成了,即用于遠(yuǎn)程調(diào)用的 Invoker。
然后通過 cluster 再包裝一下得到 Invoker,因此一個(gè)服務(wù)可能有多個(gè)提供者,最終在 ProviderConsumerRegTable 中記錄這些信息,然后返回 Invoker。
所以我們知道Conusmer 是在 RegistryProtocol#refer 中向注冊中心注冊自己的信息,并且訂閱 Provider 和配置的一些相關(guān)信息,我們看看訂閱返回的信息是怎樣的。
拿到了Provider的信息之后就可以通過監(jiān)聽觸發(fā) DubboProtocol# refer 了(具體調(diào)用哪個(gè) protocol 還是得看 URL的協(xié)議的,我們這里是 dubbo 協(xié)議),整個(gè)觸發(fā)流程我就不一一跟一下了,看下調(diào)用棧就清楚了。
終于我們從注冊中心拿到遠(yuǎn)程Provider 的信息了,然后進(jìn)行服務(wù)的引入。
這里的重點(diǎn)在 getClients,因?yàn)榻K究是要跟遠(yuǎn)程服務(wù)進(jìn)行網(wǎng)絡(luò)調(diào)用的,而 getClients 就是用于獲取客戶端實(shí)例,實(shí)例類型為 ExchangeClient,底層依賴 Netty 來進(jìn)行網(wǎng)絡(luò)通信,并且可以看到默認(rèn)是共享連接。
getSharedClient 我就不分析了,就是通過遠(yuǎn)程地址找 client ,這個(gè) client 還有引用計(jì)數(shù)的功能,如果該遠(yuǎn)程地址還沒有 client 則調(diào)用 initClient,我們就來看一下 initClient 方法。
而這個(gè)connect最終返回 HeaderExchangeClient里面封裝的是 NettyClient 。
然后最終得到的 Invoker就是這個(gè)樣子,可以看到記錄的很多信息,基本上該有的都有了,我這里走的是對應(yīng)的服務(wù)只有一個(gè) url 的情況,多個(gè) url 無非也是利用 directory和 cluster再封裝一層。
最終將調(diào)用 return (T) proxyFactory.getProxy(invoker); 返回一個(gè)代理對象,這個(gè)就不做分析了。
到這里,整個(gè)流程就是分析完了,不知道大家清晰了沒?我再補(bǔ)充前面的圖,來一個(gè)完整的流程給大家再過一遍。
小結(jié)相信分析下來整個(gè)流程不難的,總結(jié)地說無非就是通過配置組成 URL ,然后通過自適應(yīng)得到對于的實(shí)現(xiàn)類進(jìn)行服務(wù)引入,如果是注冊中心那么會向注冊中心注冊自己的信息,然后訂閱注冊中心相關(guān)信息,得到遠(yuǎn)程 provider的 ip 等信息,再通過netty客戶端進(jìn)行連接。
并且通過directory 和 cluster 進(jìn)行底層多個(gè)服務(wù)提供者的屏蔽、容錯(cuò)和負(fù)載均衡等,這個(gè)之后文章會詳細(xì)分析,最終得到封裝好的 invoker再通過動(dòng)態(tài)代理封裝得到代理類,讓接口調(diào)用者無感知的調(diào)用方法。
最后今天這篇文章看下來相信大家對服務(wù)的引入應(yīng)該有了清晰的認(rèn)識,其實(shí)里面還是很多細(xì)節(jié)我沒有展開分析,比如一些過濾鏈的組裝,這其實(shí)在服務(wù)暴露的文章里面已經(jīng)說了,同樣服務(wù)引用也有過濾鏈,不過篇幅有限就不展開了,抓住主線要緊。
至此我已經(jīng)帶大家先過了一遍 Dubbo 的整體概念和大致流程,介紹了 Dubbo SPI機(jī)制,并且分析了服務(wù)的暴露流程和服務(wù)引入流程,具體的細(xì)節(jié)還是得大家自己去摸索,大致的流程我都講的差不多了。
dubbo系列也快接近尾聲了,雖然我知道每次寫硬核技術(shù)看的小伙伴就少了很多,但是還是想寫完這個(gè)系列,感謝大家的支持。