點贊再看,養成習慣,微信搜一搜【三太子敖丙】關注這個喜歡寫情懷的程式設計師。
本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。
上篇文章我們已經瞭解了 Dubbo 服務暴露全過程,這篇文章我就帶著大家再來看看 Dubbo 服務引入全流程,這篇服務引入寫完下一篇就要來個全鏈路打通了,看看大家看完會不會有種任督二脈都被打通的感覺。

在寫文章的過程中丙還發現官網的一點小問題,下文中會提到。
話不多說,我們們直接進入正題。
服務引用大致流程
我們已經得知 Provider
將自己的服務暴露出來,註冊到註冊中心,而 Consumer
無非就是通過一波操作從註冊中心得知 Provider 的資訊,然後自己封裝一個呼叫類和 Provider 進行深入地交流。
而之前的文章我都已經提到在 Dubbo
中一個可執行體就是 Invoker
,所有呼叫都要向 Invoker 靠攏,因此可以推斷出應該要先生成一個 Invoker,然後又因為框架需要往不侵入業務程式碼的方向發展,那我們的 Consumer 需要無感知的呼叫遠端介面,因此需要搞個代理類,包裝一下遮蔽底層的細節。
整體大致流程如下:

服務引入的時機
服務的引入和服務的暴露一樣,也是通過 spring 自定義標籤機制解析生成對應的 Bean,Provider Service 對應解析的是 ServiceBean 而 Consumer Reference 對應的是 ReferenceBean。

前面服務暴露的時機我們上篇文章分析過了,在 Spring 容器重新整理完成之後開始暴露,而服務的引入時機有兩種,第一種是餓漢式,第二種是懶漢式。
餓漢式是通過實現 Spring 的InitializingBean
介面中的 afterPropertiesSet
方法,容器通過呼叫 ReferenceBean
的 afterPropertiesSet
方法時引入服務。
懶漢式是隻有當這個服務被注入到其他類中時啟動引入流程,也就是說用到了才會開始服務引入。
預設情況下,Dubbo 使用懶漢式引入服務,如果需要使用餓漢式,可通過配置 dubbo:reference 的 init 屬性開啟。
我們可以看到 ReferenceBean
還實現了FactoryBean
介面,這裡有個關於 Spring 的面試點我帶大家分析一波。
BeanFactory 、FactoryBean、ObjectFactory
就是這三個玩意,我單獨拿出來說一下,從字面上來看其實可以得知BeanFactory
、ObjectFactory
是個工廠而FactoryBean
是個 Bean。
BeanFactory
其實就是 IOC 容器,有多種實現類我就不分析了,簡單的說就是 Spring 裡面的 Bean 都歸它管,而FactoryBean
也是 Bean 所以說也是歸 BeanFactory 管理的。
那 FactoryBean
到底是個什麼 Bean 呢?它其實就是把你真實想要的 Bean 封裝了一層,在真正要獲取這個 Bean 的時候容器會呼叫 FactoryBean#getObject() 方法,而在這個方法裡面你可以進行一些複雜的組裝操作。
這個方法就封裝了真實想要的物件複雜的建立過程。

到這裡其實就很清楚了,就是在真實想要的 Bean 建立比較複雜的情況下,或者是一些第三方 Bean 難以修改的情形,使用 FactoryBean 封裝了一層,遮蔽了底層建立的細節,便於 Bean 的使用。
而 ObjectFactory 這個是用於延遲查詢的場景,它就是一個普通工廠,當得到 ObjectFactory 物件時,相當於 Bean 沒有被建立,只有當 getObject() 方法時,才會觸發 Bean 例項化等生命週期。
主要用於暫時性地獲取某個 Bean Holder 物件,如果過早的載入,可能會引起一些意外的情況,比如當 Bean A 依賴 Bean B 時,如果過早地初始化 A,那麼 B 裡面的狀態可能是中間狀態,這時候使用 A 容易導致一些錯誤。
總結的說 BeanFactory 就是 IOC 容器,FactoryBean 是特殊的 Bean, 用來封裝建立比較複雜的物件,而 ObjectFactory 主要用於延遲查詢的場景,延遲例項化物件。
服務引入的三種方式
服務的引入又分為了三種,第一種是本地引入、第二種是直接連線引入遠端服務、第三種是通過註冊中心引入遠端服務。

本地引入不知道大家是否還有印象,之前服務暴露的流程每個服務都會通過搞一個本地暴露,走 injvm 協議(當然你要是 scope = remote 就沒本地引用了),因為存在一個服務端既是 Provider 又是 Consumer 的情況,然後有可能自己會呼叫自己的服務,因此就弄了一個本地引入,這樣就避免了遠端網路呼叫的開銷。
所以服務引入會先去本地快取找找看有沒有本地服務。
直連遠端引入服務,這個其實就是平日測試的情況下用用,不需要啟動註冊中心,由 Consumer 直接配置寫死 Provider 的地址,然後直連即可。
註冊中心引入遠端服務,這個就是重點了,Consumer 通過註冊中心得知 Provider 的相關資訊,然後進行服務的引入,這裡還包括多註冊中心,同一個服務多個提供者的情況,如何抉擇如何封裝,如何進行負載均衡、容錯並且讓使用者無感知,這就是個技術活。
本文用的就是單註冊中心引入遠端服務,讓我們來看看 Dubbo 是如何做的吧。
服務引入流程解析
預設是懶漢式的,所以服務引入的入口就是 ReferenceBean 的 getObject 方法。

可以看到很簡單,就是呼叫 get 方法,如果當前還沒有這個引用那麼就執行 init 方法。
官網的一個小問題
這個問題就在 if (ref == null)
這一行,其實是一位老哥在除錯的時候發現這個 ref 竟然不等於 null,因此就進不到 init 方法裡面除錯了,後來他發現是因為 IDEA 為了顯示物件的資訊,會通過 toString 方法獲取物件對應的資訊。
toString 呼叫的是 AbstractConfig#toString,而這個方法會通過反射呼叫了 ReferenceBean 的 getObject 方法,觸發了引入服務動作,所以說到斷點的時候 ref != null
。

可以看到是通過方法名來進行反射呼叫的,而 getObject 就是 get 開頭的,因此會被呼叫。
所以這個哥們提了個 PR,但是一開始沒有被接受,一位 Member 認為這不是 bug, idea 設定一下不讓呼叫 toString 就好了。

不過另一位 Member 覺得這個 PR 挺好的,並且 Dubbo 專案二代掌門人北緯30也發話了,因此這個 PR 被受理了。

至此我們已經知道這個小問題了,然後官網上其實也寫的很清楚。

但是小問題來了,之前我在文章提到我的原始碼版本是 2.6.5,是在 github 的 releases 裡面下的,這個 tostring 問題其實我挺早之前就知道了,我想的是我 2.6.5 穩的一批,誰知道翻車了。

我除錯的時候也沒進到 init 方法因為 ref 也沒等於 null,我就奇怪了,我裡面去看了下 toString 方法,2.6.5版本竟然沒有修改?沒有將 getObject 做過濾,因此還是被呼叫了。
我又開啟了2.7.5版本的程式碼,發現是修改過的判斷。

我又去特意下了 2.6.6 版本的程式碼,發現也是修改過的,因此這個修改並不是隨著 2.6.5 版本釋出,而是 2.6.6,除非我下的是個假包,這就是我說的小問題了,不過影響不大。
其實提到這一段主要想說的是那個 PR,作為一個開源軟體的輸出者,很多細節也是很重要的,這個問題其實很影響原始碼的除錯,因為對程式碼不熟,肯定會一臉懵逼,誰知道是不是哪個後臺執行緒非同步引入了呢。
提這個 PR 的老哥花了兩個小時才搞清楚真正的原因,所以說雖然這不是個 bug 但是很影響那些想深入瞭解 Dubbo 內部結構的同學們,這種改配置去適應的方案是不可取了,還好最終的方案是改程式碼。
好了讓我們回到今天的主題,接下來分析的就是那個不讓我進去的 init 方法了。
原始碼分析
init 方法很長,不過大部分就是檢查配置然後將配置構建成 map ,這一大段我就不分析了,我們直接看一下構建完的 map 長什麼樣。

然後就進入重點方法 createProxy,從名字可以得到就是要建立的一個代理,因為程式碼很長,我就一段一段的分析。

如果是走本地的話,那麼直接構建個走本地協議的 URL 然後進行服務的引入,即 refprotocol.refer,這個方法之後會做分析,本地的引入就不深入了,就是去之前服務暴露的 exporterMap 拿到服務。

如果不是本地,那肯定是遠端了,接下來就是判斷是點對點直連 provider 還是通過註冊中心拿到 provider 資訊再連線 provider 了,我們分析一下配置了 url 的情況,如果配置了 url 那麼不是直連的地址,就是註冊中心的地址。

然後就是沒配置 url 的情況,到這裡肯定走的就是註冊中心引入遠端服務了。

最終拼接出來的 URL 長這樣。

可以看到這一部分其實就是根據各種引數來組裝 URL ,因為我們的自適應擴充套件都需要根據 URL 的引數來進行的。

至此我先畫個圖,給大家先捋一下。

這其實就是整個流程了,簡述一下就是先檢查配置,通過配置構建一個 map ,然後利用 map 來構建 URL ,再通過 URL 上的協議利用自適應擴充套件機制呼叫對應的 protocol.refer 得到相應的 invoker 。
在有多個 URL 的時候,先遍歷構建出 invoker 然後再由 StaticDirectory 封裝一下,然後通過 cluster 進行合併,只暴露出一個 invoker 。
然後再構建代理,封裝 invoker 返回服務引用,之後 Comsumer 呼叫的就是這個代理類。
相信通過圖和上面總結性的簡述已經知道大致的服務引入流程了,不過還是有很多細節,比如如何從註冊中心得到 Provider 的地址,invoker 裡面到底是怎麼樣的?別急,我們繼續看。
從前面的截圖我們可以看到此時的協議是 registry 因此走的是 RegistryProtocol#refer,我們來看一下這個方法。

主要就是獲取註冊中心例項,然後呼叫 doRefer 進行真正的 refer。

這個方法很關鍵,可以看到生成了RegistryDirectory
這個 directory 塞了註冊中心例項,它自身也實現了NotifyListener
介面,因此註冊中心的監聽其實是靠這傢伙來處理的。
然後向註冊中心註冊自身的資訊,並且向註冊中心訂閱了 providers 節點、 configurators 節點 和 routers 節點,訂閱了之後 RegistryDirectory 會收到這幾個節點下的資訊,就會觸發 DubboInvoker 的生成了,即用於遠端呼叫的 Invoker。
然後通過 cluster 再包裝一下得到 Invoker,因此一個服務可能有多個提供者,最終在 ProviderConsumerRegTable 中記錄這些資訊,然後返回 Invoker。
所以我們知道Conusmer
是在 RegistryProtocol#refer 中向註冊中心註冊自己的資訊,並且訂閱 Provider 和配置的一些相關資訊,我們看看訂閱返回的資訊是怎樣的。

拿到了Provider
的資訊之後就可以通過監聽觸發 DubboProtocol# refer 了(具體呼叫哪個 protocol 還是得看 URL的協議的,我們這裡是 dubbo 協議),整個觸發流程我就不一一跟一下了,看下呼叫棧就清楚了。

終於我們從註冊中心拿到遠端Provider
的資訊了,然後進行服務的引入。

這裡的重點在 getClients
,因為終究是要跟遠端服務進行網路呼叫的,而 getClients 就是用於獲取客戶端例項,例項型別為 ExchangeClient,底層依賴 Netty 來進行網路通訊,並且可以看到預設是共享連線。

getSharedClient
我就不分析了,就是通過遠端地址找 client ,這個 client 還有引用計數的功能,如果該遠端地址還沒有 client 則呼叫 initClient,我們就來看一下 initClient 方法。

而這個connect
最終返回 HeaderExchangeClient
裡面封裝的是 NettyClient
。

然後最終得到的 Invoker
就是這個樣子,可以看到記錄的很多資訊,基本上該有的都有了,我這裡走的是對應的服務只有一個 url 的情況,多個 url 無非也是利用 directory
和 cluster
再封裝一層。

最終將呼叫 return (T) proxyFactory.getProxy(invoker);
返回一個代理物件,這個就不做分析了。
到這裡,整個流程就是分析完了,不知道大家清晰了沒?我再補充前面的圖,來一個完整的流程給大家再過一遍。

小結
相信分析下來整個流程不難的,總結地說無非就是通過配置組成 URL ,然後通過自適應得到對於的實現類進行服務引入,如果是註冊中心那麼會向註冊中心註冊自己的資訊,然後訂閱註冊中心相關資訊,得到遠端 provider
的 ip 等資訊,再通過netty
客戶端進行連線。
並且通過directory
和 cluster
進行底層多個服務提供者的遮蔽、容錯和負載均衡等,這個之後文章會詳細分析,最終得到封裝好的 invoker
再通過動態代理封裝得到代理類,讓介面呼叫者無感知的呼叫方法。
最後
今天這篇文章看下來相信大家對服務的引入應該有了清晰的認識,其實裡面還是很多細節我沒有展開分析,比如一些過濾鏈的組裝,這其實在服務暴露的文章裡面已經說了,同樣服務引用也有過濾鏈,不過篇幅有限就不展開了,抓住主線要緊。
至此我已經帶大家先過了一遍 Dubbo
的整體概念和大致流程,介紹了 Dubbo SPI
機制,並且分析了服務的暴露流程
和服務引入
流程,具體的細節還是得大家自己去摸索,大致的流程我都講的差不多了。
dubbo系列也快接近尾聲了,雖然我知道每次寫硬核技術看的小夥伴就少了很多,但是還是想寫完這個系列,感謝大家的支援。

我是敖丙,你知道的越多,你不知道的越多,我們下期見!
人才們的 【三連】 就是敖丙創作的最大動力,如果本篇部落格有任何錯誤和建議,歡迎人才們留言!
文章持續更新,可以微信搜一搜「 三太子敖丙 」第一時間閱讀,回覆【資料】有我準備的一線大廠面試資料和簡歷模板,本文 GitHub https://github.com/JavaFamily 已經收錄,有大廠面試完整考點,歡迎Star。