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

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

















































