Hola,我是 yes。
經過了 RPC 核心和 Dubbo 微核心兩篇文章後,今天終於要稍稍深入一波 Dubbo 了。
作為一個通用的 RPC 框架,效能是很重要的一環,而易用性和擴充套件性也極為重要。
簡單地、無侵入式地擴充套件和定製 RPC 各階段功能是很多團隊的述求,Dubbo 就滿足了這些需求。
它通過微核心設計和 SPI 擴充套件,使得一些有特殊需求的業務團隊可以在 Dubbo 中實現自己的擴充套件,而不需要修改原始碼。
Dubbo 的成功離不開這樣的設計,今天我們們就來盤一盤 Dubbo 是如何實現無侵入擴充套件的,其間還會看到 Dubbo 的 IOC 和 AOP。
還有先打個預防針,今天的內容程式碼有點多的,畢竟想要深入剖析,原始碼必不可少,剛好也順帶提一下看原始碼的小技巧。
所以建議電腦上看,更加清晰和舒適。
還有如果有沒看過原始碼的同學,來緊跟 Dubbo 這個系列吧,到時候再也不怕被面試官問看過原始碼沒了。
SPI
Dubbo 就是利用 SPI (Service Provider Interface)來實現擴充套件機制的。
這個 SPI 想必你們都很熟悉,在大學寫資料庫大作業的時候就碰到了,訪問資料庫需要用到 java.sql.Driver
。
市面上的資料庫五花八門,每個資料庫廠商都有自己的實現,所以肯定需要定製一個介面,這樣我們面向介面程式設計即可。
而具體的實現則可以通過配置來載入,JDK SPI 這時候就派上用場了。
其實一點都不神奇,就是約定一個地方,載入的時候就去那個地方找實現類。
約定一個地方直白點說就是程式碼裡面寫死了一個目錄,這個目錄就是 META-INF/services/
。
然後在這個目錄下建立一個檔案,用介面全限定名來命名,檔案內容就是實現類的全限定名。
到時候要實現類就根據介面名來這裡找,然後例項化就行了。
挺簡單的吧,這就是 JDK SPI,但是它不滿足 Dubbo 的需求。
因為 Dubbo 把自身的一些實現也剝離出來成為擴充套件,而這些實現還是有點多的,也不需要全部用上。
如果用 JDK SPI 會把配置檔案裡面的類全部載入,這就導致資源的浪費。用的時候還需要遍歷過去才能找到對應的實現。
所以 Dubbo 就在 JDK SPI 的基礎上實現了個 Dubbo 的 SPI,可以根據指定的名稱按需載入實現類,比如拿 Cluster 來說就有這麼多實現類。
約定的地方改了一下,一共有三個目錄。
- META-INF/dubbo/internal/ :這裡是存放 Dubbo 內部使用的 SPI 配置檔案。
- META-INF/dubbo/ :這裡是存放使用者自定義 SPI 配置檔案。
- META-INF/services/:相容 JDK SPI
然後檔案裡面的內容是key=value
形式,這樣就可以根據 key 找到對應的實現類。
然後在註解上可以配置預設的 key 來選擇預設的實現類,比如 Cluster 預設的實現是 failover。
也可以通過 URL 引數來選擇實現類。
還有像 JDK SPI 擴充套件點載入失敗的話,連擴充套件點名稱都拿不到,到時候報錯也不知道哪裡出問題。
而 Dubbo SPI 則不會吃了錯誤,並且還提供了擴充套件點的自動注入和 AOP 功能。
大致瞭解了 Dubbo SPI 之後,我們再來深入看看實現細節。
Dubbo SPI 實現細節
Dubbo SPI 的核心實現在 ExtensionLoader 中,它負責擴充套件點的載入和生命週期的維護,類似 JDK SPI 的 ServiceLoader。
這裡要先提一點看原始碼的小技巧了。
開源框架都會有單元測試,而單元測試裡面就會有我們看原始碼時候想要的各種功能實現,我們就可以從單元測試入手得知一些功能的劃分,然後斷點除錯逐漸深入。
比如今天文章的 ExtensionLoader ,它在 dubbo-common 模組中,我們們就進入 test 來看看它測試用例怎麼寫的。
當然除了通過資料夾來找,直接用檔名搜也行。
找到了就好辦了,資料都是造好的,找到你想要除錯的方法,斷點一設,箭頭一點,這不就美滋滋了嗎?
好了,小技巧分享完畢,回到 ExtensionLoader,我們簡單點就用 Dubbo 單元測試的資料來看看實現。
有個叫 SimpleExt 的類,有三個實現,預設的實現是 impl1。
再來看看 SPI 配置檔案的內容,可以看到為了測試還故意寫了一些空格在配置檔案中。
然後現在如果要找 impl2 這個實現,通過以下程式碼呼叫即可。
SimpleExt ext = ExtensionLoader
.getExtensionLoader(SimpleExt.class).getExtension("impl2")
一個擴充套件介面對應有個 ExtensionLoader,找到對應的 ExtensionLoader,然後再載入對應名字的實現類。
接下來會有原始碼,不過沒關係,還是很簡單的,想要深入原始碼這關必須過。
可以看到getExtensionLoader
是靜態的,裡面邏輯也很簡單就是從快取找介面對應的 ExtensionLoader,找不到就新建一個返回。
現在有了 ExtensionLoader,我們們再來看看 getExtension 的邏輯,來看看是如何通過擴充套件點 name 找到對應的實現類的。
可以看到又是有個快取操作,邏輯非常簡單,先去快取找例項,如果沒有則建立例項。
要說細節就是用到了雙檢鎖,然後用 holder 來保證可見性和防止指令重排。應該看到註釋上的 holder 構造了吧,volatile 和雙檢鎖的搭配,這裡就不深入了。
我們來看看 createExtension,這是要建立擴充套件點了,程式碼有點長,但是我都做了相應的註釋,包括綠色的註釋。
邏輯還是很簡單的,詳細的程式碼沒有具體展示,我先口述一下。
- 通過介面類名去三個目錄找到對應的檔案。
- 解析檔案內容生成 class 物件,然後快取到 cachedClasses 中。
- 然後通過 name 去 cachedClasses 中找到對應的 class 物件。
- 去快取 EXTENSION_INSTANCES 看看是否已經例項化過了。
- 沒有的話就例項化,然後呼叫 injectExtension 實現自動注入。
- 再通過 cachedWrapperClasses 實現包裝,將最後的包裝類返回。
有幾點不清晰沒關係,我們們接著分析,腦海中先大概知道要做什麼,然後再來看看具體是怎麼做的。
原始碼中的 loadDirectory 就是去目錄找檔案,然後解析,最終會呼叫 loadClass,這個方法很關鍵,我們詳細分析一下,為了便於觀看,刪除了一些程式碼。
自適應我們們先略過,只要知道是在這裡記錄的即可。
然後上面提到的 AOP 相關的 cachedWrapperClasses 就是在這裡記錄的,如果判斷它是包裝類呢?
簡單粗暴但是有效,只要有當前類作為構造器引數的類就是包裝類,有點拗口,多讀幾遍就理解了。
現在我們再回過頭來看看這段程式碼,Dubbo 的 AOP。
把擴充套件類對應的包裝類都記錄下來放在 cachedWrapperClasses 中,然後在例項化擴充套件類的時候就一層一層的把擴充套件類包起來,最終返回的就是包裝類。
為什麼說這就是 AOP 呢?因為等於把一些邏輯切進了擴充套件實現類中。
其實就是把擴充套件物件的公共邏輯移到包裝類中,我們看下單元測試的例子就很清晰了。
從圖中可以看到有兩個擴充套件實現類,兩個包裝類,具體邏輯就不看了,不是重點,配置檔案如下:
然後再看一下單元測試的執行結果,可以看到最終返回的其實是 Ext5Wrapper1 物件,並且它還包著 wrapper2 物件。
所以 echo 方法的呼叫鏈就是:
Ext5Wrapper1 ->Ext5Wrapper2->Ext5impl1
也就起到了 AOP 的效果。
接下來我們再來看看 injectExtension,是如何實現 Dubbo 的自動注入。
看了程式碼之後是不是有點失望,就這?
是的就是這麼樸素地判斷有沒有 set 方法,然後根據引數找到物件,執行 set 方法注入即可。
所以說原始碼之下無祕密,看起來好像很高階的東西,就這。
上面程式碼中還有個objectFactory.getExtension()
,這個和擴充套件自適應有關係,還有個@Activate
也沒說。
這些內容還是有點多的,也很重要,感覺上可能還有點繞,所以單獨寫一篇說。
最後
Dubbo 就是靠自己實現的 SPI 機制把通訊協議、序列化格式、負載均衡、路由策略等各部分抽出來作為外掛,實現擴充套件和定製。
通過微核心和SPI 機制來滿足使用者定製化的需求,也保證了框架本身的穩定性和可持續性。
並且 Dubbo 自身也提供了很多已有的實現,像各種路由策略等等。
所以說一個好的框架不僅自己功能要全,還得對擴充套件開放,這樣生態才會壯大。
今天的程式碼還是有點多的,如果看不懂的建議下載原始碼,跟著原始碼除錯幾遍就清晰了。
原始碼這一步一定要邁過去,邁過去了之後就輕鬆了。
Dubbo 系列持續更新,敬請期待,有問題可以留言,我會盡量解答。
歡迎關注我的個人公眾號,文章首發公眾號。
更多文章可看我的文章彙總:https://github.com/yessimida/yes 歡迎 star !
我是 yes,從一點點到億點點,歡迎在看、轉發、留言,我們下篇見。