Dubbo源代碼分析-SPI

關於爲何

    很早就接觸Dubbo,相信不少同窗也是很早就接觸Dubbo,Dubbo的出現解決不少公司的系統通信問題,讓很公司從通信層面得以解脫,由於能自研RPC框架畢竟是少數,就算是自研RPC框架,Dubbo也是一個重要的參考對象。可是因爲阿里官方中止維護Dubbo,讓又那些已使用Dubbo的公司比較被動,雖然噹噹後來維護DubboX,但也沒有更多的發展。後來SpringCloud出現讓你們眼前一亮,不少公司直接轉向SpringCloud技術體系了,由於SpringCloud擁有頭上微服務光環和全棧技術體系(這裏先不展開討論SpringCloud技術與微服務)。後來隨着阿里雲的發展,阿里也意識中止維護Dubbo是一件事很是錯誤作法,後來不只繼續維護而且把Dubbo捐給Apache基金會,這樣之後中止維護可能性就不大了。在此期間,由於工做的緣由,使用了Dubbo和SrpingCloud,期間還一度放棄使用Dubbo,後來又基於Dubbo改造和開發了服務治理等功能,如下這幾點是我熟悉源Dubbo代碼的我的觀點。程序員

  • 由於剛開始在項目上使用Dubbo,有異常時,沒法快速定位問題和處理問題,特別是線上的出問題的時候。若是直接百度(或者GG)這些異常或問題,答案每每都是各不相同的,而官方也沒有一個明確的說法(由於官方停擺了)。這是一個很是現實的問題,無論是框架層面的問題仍是使用層面的問題,都須要解決或定位後規避這些問題,並且Dubbo屬於一個基礎服務,被全部系統直接依賴,深刻了解必須的,直接從源層面是最有效的,這樣在使用Dubbo層面上技術風險可控。
  • SpringCloud技術體系的通信是以Http爲基礎,使用方便可是效率相對TCP(Dubbo)並不高。也許不少同窗以爲這個沒問題,固然,若是是一個小型的系統,確定不會有問題,可是若是是中大型的分佈式系統,平均響應延時差距就出來了。特別是當前微服務技術體系中,不少同窗會把系統拆得很散,調用鏈路變長,那通信效率就值你們去考慮了。
  • 由於服務多了,調用鏈路變長,服務治理就是一個很是關鍵的點,好比:服務的降級、限流、熔斷,這些功能對系統穩定和高可用是決定性的做用(固然,高可用涉及的點不少,但本文章不會涉及)。相對於Hystrix的組件服務治理方案,平臺化的服務治理有着優秀特色。可是平臺化服務治理方案怎麼處理,這個問題就很難繞過PRC框架,或者說平臺化的服務治理和RPC框架有着自然的優點組合。其實,Dubbo已經有一小部分服務治理功能,這個是Dubbo相對其它RPC優點,但這個優點目前還不完善。
  • 系統的灰度發佈和全鏈路壓測。由於灰度發佈和全鏈路壓測也直接依賴了RPC框架,灰度發佈時涉及服務調用鏈用、服務路由、服務註冊的版本號(與註冊中心與有關係)、數據雙寫等,這些也能夠歸爲RPC範疇。經過對RPC協議頭部信息設計和利用,就能夠完成灰度發佈和開展全鏈路壓測,固然還有系統的監控等事宜。
  • 最後做爲一各技術人員或者程序員,出於對上面這些技術好奇,同時很想知道和深刻了解這些技術究竟是怎麼實現,對技術本質渴望和追求,深刻研究優秀開源框架和解決方案,結合本身的業務特色再造適合本身輪子(對開源的改造和二次開發),這是一名程序員應該作的事,也是我花不少時間在Dubbo源代碼上面的重要緣由。

    也許能夠從不少文章能夠看到Dubbo的原理或者流程之類的介紹。無疑,瞭解架構和原理是好的,瞭解工做流程也是好的,也應該這樣作。可是我以爲源代碼層面是來得最直接的和直觀的,正如前輩Linux創始人 Linus Torvalds說的"Talk is cheap. Show me the code"。Dubbo源代碼文章一方面屬於我的的總結,另外一方面是出於分享精神而爲之,但願經過輸出加深本身理解,同時也能幫助到有須要的同窗。固然,在熟悉Dubbo過程當中,官方的文檔對我幫助是很是大,在此向Dubbo開發團隊致敬。另外,本文基於Dubbo2.7.4版本,這一點很是重要,由於自從Dubbo從新維護後,迭代速度仍是很快的。面試

SPI是什麼

    引用下wiki的"Service Provider Interface (SPI) is an API intended to be implemented or extended by a third party. It can be used to enable framework extension and replaceable components."。翻譯:服務提供商接口(SPI)是旨在由第三方實現或擴展的API。 它可用於啓用框架擴展和可替換組件。SPI 以接口的全限定名爲"META-INF/services"目錄下的文件名,而後接口的實現類配置在全限定名配置中,由服務加載器讀取配置文件,加載實現類。在程序運行時,動態爲接口替換實現類。經過 SPI 機制爲咱們的程序提供拓展功能或替換組件,Dubbo 就是也利用 SPI 機制實現擴展和解耦的,只是Dobbu並無直接使用Java原生SPI,而是從新實現了一套功能更強的 SPI 機制。apache

    Java原生SPI的示例參考:http://www.javashuo.com/article/p-rzsilkhb-bu.html設計模式

Dubbo的SPI

    Dubbo並無直接採用Java SPI,而是從新實現了一套功能SPI。首先,Dubbo經過Key=Value方式組織方式SPI,這樣能夠直接根據須要來獲取實現類,而不是遍歷全部實現者。其次,Dubbo經過還增長IOC和AOP功能特性。最後,Dubbo沒有直接使用ServiceLoader加載類,而是經過ClassUtils.getClassLoader獲取ClassLoader加載的(這一點後面會具體分析)。緩存

爲何須要SPI

    從上面的定義也能夠看得出來,使用SPI的核心緣由是擴展和解耦。可是Java原生的SPI不夠靈活、強大,因此Dubbo本身實一套SPI機制。SPI是很是重要的一個機制,若是要熟悉和學習Dubbo的源代碼,就必須瞭解和熟悉SPI,後面不少環節和功能都看到SPI的實現,否則在閱讀源代碼時會出現不知道代碼怎麼出來的問題。安全

Dubbo SPI組成

    在Dubbo在實現的SPI代碼實現主要在ExtensionLoader類實現的,SPI功能包括兩部份內容,第一部分是ExtensionLoader實例的建立,第二部分是拓展類對象的獲取。其中,第二部分是本節重點內容,第一部分會簡單分析描述。架構

代碼分析

建立ExtensionLoader實例

邏輯流程圖

    這段邏輯最要的是:在建立全部ExtensionLoader實例時,必須先建立ExtensionFactory的ExtensionLoader實例,這一點是在看代碼必須的注意,否則很容易誤解。其它的邏輯條件判斷或流程都比較簡單,也容易理解。app

    在建立ExtensionLoader實例時,會調用代碼getAdaptiveExtension獲取自適應拓展,這個一塊先略過,由於獲取自適應拓展是一個比較大的主題,須要單獨分析。框架

調用鏈流程圖

    這塊沒有多少行代碼,並且邏輯比較簡單,直接看代碼就能夠了。以下圖分佈式

拓展類對象的獲取

邏輯流程圖

    上圖標註爲紅色部分是關鍵邏輯部分,須要特別注意的地方。

    主要邏輯是經過獲取對象或Class對象時,若是沒有相關的對象不存在,若是緩存沒有就建立,建立成功後就保存至緩存。

    在建立Clazz對象以前,先加載Dubbo SPI的配置文件,而後解釋配置文件,並根據Class的類型緩存至不相同類型的變量。Dubbo很基礎服務和功能都是通SPI實現功能和擴展的,如:Protocol、ZookeeperTransporter等。下面是V2.7.4的Dubbo版本的SPI部分配置文件:

META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol
    META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol
    META-INF/dubbo/org.apache.dubbo.rpc.Protocol
    META-INF/dubbo/com.alibaba.dubbo.rpc.Protocol
    META-INF/services/org.apache.dubbo.rpc.Protocol
    META-INF/services/com.alibaba.dubbo.rpc.Protocol
    META-INF/dubbo/internal/com.alibaba.dubbo.remoting.zookeeper.ZookeeperTransporter
    META-INF/dubbo/org.apache.dubbo.remoting.zookeeper.ZookeeperTransporter
    META-INF/dubbo/com.alibaba.dubbo.remoting.zookeeper.ZookeeperTransporter
    META-INF/services/org.apache.dubbo.remoting.zookeeper.ZookeeperTransporter
    META-INF/services/com.alibaba.dubbo.remoting.zookeeper.ZookeeperTransporter

    從上圖能夠看到,在同一個目錄下,存在相同的類名,可是包名不同,如:com.alibaba.dubbo.rpc.Protocol和org.apache.dubbo.rpc.Protocol,緣由是Dubbo移交給Apache社區後,就啓用com.apache.dubbo了,爲了保持向後兼容,仍然存在com.alibaba.dubbo.xxx的類。

    解釋配置文件後,就是根據加載經過ClassLoader建立Class對象,再根據Class類型分別保存不一樣的緩存對象,cachedAdaptiveClass、cachedWrapperClasses、cachedNames等緩存容器。

    建立對象後,還要注入依賴,而且建立對象的包裝類。注入依賴是經過反射和set方法注入的。固然,包裝類反射和set方法也要注入依賴。

調用鏈流程圖

    上圖是Dubbo SPI的代碼調用鏈,畫這個圖是方便總結調用鏈,也能夠直觀看到SPI涉及此類和調用時序。Dubbo SPI涉及到類和繼承還算比較簡單,服務的導入、導出模塊就很是複雜,有了這個圖就會比較快速、直觀方式理解這個功能的邏輯。

    獲取擴展對象代碼入口「ExtensionLoader.getExtension」,咱們先明確代碼入口方便後面在閱讀代碼跟蹤。雖然SPI這塊的代碼層次不復雜,可是後面服務集羣、服務對象的導出等有着比較複雜代碼層次,明確下代碼入口有利於咱們方便閱讀代碼。

    獲取對象時先從緩存獲取,若是緩存沒有就建立,建立成功後就保存至緩存,很典型的懶漢模式建立單例。Dubbo大部分實例都都是單例的,而且經過懶漢模式建立,建立時經過double checked方式保證安全線程,這是咱們讀Dubbo代碼須要注意的。以下圖:

    PS:在平時招聘面試的過程當中,不少同窗只是知道單例建立的二種方式,但有至關多一部分同窗是不知道怎麼正確地使用懶漢模式建立單例

    方法"ExtensionLoader.loadExtensionClasses"是解釋SPI配置文件的入口,首先緩存默認擴展名,這個從方法名"cacheDefaultExtensionName"就能夠看出來。

    從圖能夠看出來loadExtensionClasses有那些功能,主要是緩存默認擴展類和加載指定的配置文件。其中,加載指定的配置目錄下文件是咱們要重點了解的內容,系統經過常量指定了三個目錄:

//META-INF/dubbo/internal/
//META-INF/dubbo/internal/
//META-INF/services/

從這三組配置目錄下,均使用兼容方式加載配置文件, 這個上面說過,就不囉嗦了。

    這個方法有一個小細節,就是局部變量"extensionClasses"變量使用HashMap建立的,而不是ConcurrentHashMap。從這一點能夠看出寫段代碼的同窗並無濫用ConcurrentHashMap,而是在安全的狀況使用HashMap,可是這個類ExtensionLoader的全局變量而且用於緩存對象則大量使用ConcurrentHashMap,達到性能與線程安全的統一。我的認爲雖然這個地方使用HashMap並不比ConcurrentHashMap也不會系統節省不少資源,更不會是系統的瓶頸,可是養成這種習慣選擇最優的方式實現功能並非什麼壞事,並且使用從小處着手能夠看得出一個程序員的功底。另外,Dubbo仍是有的細節處理得不夠好,這個問題咱們會在後面的指出和討論。

    方法"ExtensionLoader.cacheDefaultExtensionName"主要功能是設置cachedDefaultName值,比較簡單,直接看代碼就行了。

    方法「ExtensionLoader.loadDirectory」是須要重點了解的過程,主要包括幾部分,1、獲取ClassLoader;2、加載配置文件獲得urls;

在獲取ClassLoader的時候,一共經過三種方式獲取,以下圖:

  • 首先是通"Thread.currentThread().getContextClassLoader()"獲取ClassLoader,由於可用於破壞雙親委派模式,而且解決類加載器的代理模式(雙親委派)沒法加載的類的問題。是的,雙新委派模型在某種狀況下是能夠破壞,如:Jdbc等狀況。
  • 若是上面的方式獲取不到ClassLoader,則直接當前Clazz的ClassLoader。
  • 最後是經過ClassLoader.getSystemClassLoader系統類加載器,這個是系統類加載器。

    這裏經過三種方式獲取ClassLoader,以保證ClassLoader不爲空。由於ClassLoader深層次解釋涉及到Jvm體系結構和歷史緣由,在這裏就不展開了。

    方法"ClassLoader.loadClass"主要把clazz對象緩存起來,在調用"ClassLoader.loadClass"以前就已經經過"Class.forName(line, true, classLoader)"加載、鏈接、初始化了,緩存起來的對象供後面使用。以下圖:

    獲得實例後,就要注入依賴。先遍歷要注入instance的全部方法,而後檢查方法是否set開頭,而且沒有標識@DisableInject註解的,要注入的參數類型也不是基本數據類型。檢查經過後,經過"objectFactory.getExtension"獲取要注入對象,並經過反射方法invoke賦值。

涉及的知識點

  • SPI

    Dubbo SPI核心訴求在於擴展和解耦,SPI是Dubbo的可擴展基石,在Dubbo有着普遍的使用。

  • ClassLoader、反射

    幾種獲取ClassLoader方式,但又存在區別和聯繫。這塊內容的值得咱們去繼續深刻挖掘和研究。不少同窗對ClassLoader或雙親委派只是只知其一;不知其二,能夠借這個機會研究清楚。

  • IOC

    Dubbo的IOC比較簡單,就像當年Spring尚未註解的IOC同樣使用必須提供set方法賦值),必須經過反射的method.invoke方法和set方法賦值。

  • 設計模式(單例、雙重檢查)

    Dubbo使用的設計模式比較多,如:裝飾器模式、觀察者模式、代理模式、單例模式等。在本節主要涉單例模式,經過雙重檢查來保證明例的建立。其實,單例表面是比較簡單,可是日常在面試過程當中,還有不少同窗沒有搞明白的。Dubbo則大量運用雙重檢查的方式建立單例,你們能夠參考下。

缺點或存在問題

    Dubbo雖然在RPC方面作得很好,說是開源RPC的帶頭大哥也不爲過,並且通過阿里等衆多一線互聯網公司的大流量的驗證後,是能夠勝任分佈式系統RPC工做的,可是仍是一些美中足之處。

    Dubbo已經開源而且捐贈給Apache,那麼開源後,確定是但願更多的程序員加入社區而且共同維護Dubbo,但部分的Dubbo代碼可讀性確實差了一些。爲何說代碼可讀性差了一些,一方面是由於不少類的關鍵屬性和方法解釋不多或者甚至是沒有的,這種狀況下是不利程序員於熟悉源代碼的;另外一方面,能夠看到代碼Dubbo風格沒有統一,如:對空值的判斷、代碼的行之間的間隔、命名規範等。固然,Dubbo不是一我的的做品,會有多種代碼風格和我的習慣。可是,優秀團隊的做品應該保持相對一致,好比:代碼間隔行數、關於空的判斷、註釋的書寫,這些都應該保持相對統一,這樣方便團隊後期維護和開源有更多同窗能夠參與。固然,Dubbo團隊也發現這些問題,正在改進,而且能夠經過對比新、舊版本源看到改進的地方。

小結

    SPI章節代碼層次相對簡單、代碼結構也相對簡單,也沒有涉及太多類,基本就在ExtensionLoader完成了,可是須要搞懂它,仍是要多讀幾遍。因爲時間倉促、水平有限,其中有許多不足之處在所不免,敬請各位批評指正,給予多多指導,謝謝。

相關文章
相關標籤/搜索