Dubbo——擴展機制實現原理

主題 Dubbo

Dubbo是一款設計非常棒的框架,最近開始看其源碼,並且在這個過程中寫一款RPC框架,可以當成tiny-dubbo,主要目的是寫的過程學習設計思想.前段時間和一些朋友也聊到源碼分析文章的意義在哪裡,最後得出的結果是沒有意義,沒有解決實質問題的源碼分析是沒有必要的,如果要分析應該站在

上帝視角 ,說出他的設計,並且這樣設計的好處與壞處,而不是單純的看每一行代碼幹什麼,聊到最後有一種醒悟的感覺,之前看Spring MVC與Mybatis的源碼就是犯了這種錯誤,因此該篇開始調整閱讀姿勢.

Java SPI

聊Dubbo之前先聊一聊Java的SPI機制.關於SPI之前文章可以參考Java基礎專題的相關文章,這裡再提兩個問題:

1.SPI解決的問題是什麼?

在開發中經常有面向接口編程這一說法,接口就是協議,由調用方與實現方共同制定的契約,接口負責把調用方與實現方解耦.這其中會有一個問題實現方實現接口後怎麼告知調用方?一般來說調用方維護一個Map(bean容器),然後實現方把實現類注入進去,這種做法導致實現方每次新增一個實現類都要手動注入到調用方,這種當然是不符合開閉原則的做法,比較優雅的做法是Spring IOC,利用註解把所有實現類交給IOC管理,然後注入時根據接口就能拿到所有的實現,在運行時再根據配置自動選擇,但是缺點是依賴Spring.那麼SPI的做法就是類似Spring,但是其沒有IOC容器,因此採用在classpath下配置方式來獲取擴展點的實現類,SPI會掃描classpath下 META-INF/services 下所有實現類,然後在需要使用的時候自動實例化.

Dubbo——擴展機制實現原理

2.SPI的核心思想是什麼?

我目前理解的是 Open Close Principle(OCP) 也就是開放關閉原則的實現策略,對擴展開放對修改關閉.使用起來的直觀就是隻需要引入相關架包,然後 ServiceLoader 會自動加載該實例,在使用的時候會自動創建實例提供給用戶.整個過程如果用戶是面向接口編程則不用修改任何代碼便新增了一種該接口的實現,接著只要配置上所使用的實現即可.

對於Dubbo框架來說,其Service上有著許許多多的配置,比如下方的客戶端服務配置動態代理方式選用 javaassist ,通信方式選用 netty ,這些在運行時都需要獲取到具體的實現類來應用這些策略,並且用戶可以自定義自己的策略,那麼SPI的方式就是一種很好的插件式擴展實現.

Dubbo的擴展點設計

對於Dubbo來說一個SPI接口的Impl來源有兩處 Spring IOC 與 SPI Loader ,所經歷的過程是類加載-> 實例化(實際上是在獲取時的懶加載機制) -> 根據配置名稱獲取實例 -> 應用實例 ,下面對這個流程分析,最後有一個總的關係圖理清思路.

加載類

1. Spring IOC

對於 Spring IOC 類加載以及實例化都是由其本身來控制,Dubbo本身的設計對Spring並不是一個強依賴,獲取實例都是通過 SpringExtensionFactory 這一適配器與 ApplicationContext 建立關聯,從中取出所需要的實例.因此這裡不多分析.

2. SPI Loader

Dubbo的SPI加載核心類為 ExtensionLoader ,以獲取適配類為例,初次加載的大概流程如下:

Dubbo——擴展機制實現原理

步驟1:要獲取 ProxyFactory 的適配類(關於適配類是什麼後面會詳細說),對於Dubbo來說有 JavassistProxyFactory 與 JdkProxyFactory 以及 StubProxyFactoryWrapper 三種實現類,獲取的代碼如下:

final ProxyFactory adaptiveExtension = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();

步驟2: ExtensionLoader 的實例並不是所有SPI接口共享,每一個SPI接口都有其自己唯一的一個ExtensionLoader ,因此在該方法中使用了緩存設計,這種設計一般私有化構造器,然後利用懶漢式單例實例化,存入一個所有類共享的Map中,對於 ExtensionLoader 其實例化之後所存入的Map為 ConcurrentMap, ExtensionLoader>> .到這裡只是實例化了 ExtensionLoader 實例,並沒有觸發加載行為,加載行為會在獲取時觸發,這是一種延遲加載設計,避免加載過多不需要用到的資源.

步驟3: getAdaptiveExtension 是獲取適配類的入口,適配類與SPI也是一對一結構,因此這裡使用了 雙重檢查鎖單例 方式來創建該實例,雙重檢查需要 volatile 修飾提供可見性有序性的保證,這裡的做法是使用 Holder 這一包裝類來提供 volatile 修飾.

public T getAdaptiveExtension() { Object instance = cachedAdaptiveInstance.get(); if (instance == null) { // 由於加載時會出現異常,如果避免出現異常還一直加載,因此這裡會先預判,只要出現過異常那麼之後會一直加載失敗 if (createAdaptiveInstanceError == null) { synchronized (cachedAdaptiveInstance) { // 雙重檢查的核心,到同步塊內部還需要再判斷一次 instance = cachedAdaptiveInstance.get(); if (instance == null) { try { instance = createAdaptiveExtension(); cachedAdaptiveInstance.set(instance); } catch (Throwable t) { ... } } } } } return (T) instance;}

步驟4:步驟4 = 步驟5 + 步驟6,因此直接看步驟5

步驟5:步驟5是SPI加載流程,對於Dubbo來說加載會去 META-INF/services/ , META-INF/dubbo/, META-INF/dubbo/internal/ 三個路徑中取獲取對應實例,以 ProxyFactory 為例,dubbo的配置形式為 name -> impl ,如下形式:

stub=com.alibaba.dubbo.rpc.proxy.wrapper.StubProxyFactoryWrapperjdk=com.alibaba.dubbo.rpc.proxy.jdk.JdkProxyFactoryjavassist=com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory

該加載會放入到 Holder>> 中,key為名稱,value是對應的class實現類,這裡也用到了單例,主要原因是Dubbo的一個 ExtensionLoader 實例有太多的觸發加載入口因此需要避免多次加載帶來性能影響,執行到此時已經完成類類加載以及靜態字段,靜態塊的初始化流程,但是具體能使用的類還沒有被實例化出來.

步驟6:步驟6則是創建適配類,適配類給我一種黑科技的感覺.首先說下什麼是適配類?適配類是一種 組合設計模式的思想 (關於組合設計模式可以參考我博客設計模式專題),前面說過對於Dubbo來說一個SPI接口的Impl來源有兩處 Spring IOC 與 SPI Loader ,但是對於調用方來說這些都是無關緊要的,他只關心能不能獲取到實例,因此需要提供一個統一的調用入口,也就是組合適配類.

ExtensionFactory 是Dubbo中獲取擴展實例的入口,其主要實現類有 SpiExtensionFactory 從SPI中獲取, SpringExtensionFactory 從Spring IOC中獲取.然後提供了一個適配類來組合這兩個容器,如下代碼所示,Dubbo中適配類的標誌是標註了 @Adaptive 註解,這樣在 ExtensionLoader 加載中會自動將其標註為唯一的適配類.(

注意: Dubbo中一種SPI Class只允許有一個適配類 )

Dubbo——擴展機制實現原理

@Adaptivepublic class AdaptiveExtensionFactory implements ExtensionFactory { // 葉子節點 private final List factories; public AdaptiveExtensionFactory() { ExtensionLoader loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class); List list = new ArrayList(); for (String name : loader.getSupportedExtensions()) { list.add(loader.getExtension(name)); } factories = Collections.unmodifiableList(list); } public  T getExtension(Class type, String name) { // 從葉子節點中找到對應的實例 for (ExtensionFactory factory : factories) { T extension = factory.getExtension(type, name); if (extension != null) { return extension; } } return null; }}

組合適配類在Dubbo中有兩種實現,一種是上面 AdaptiveExtensionFactory 手動實現的適配類,另一種則是代碼生成的類,對於 ProxyFactory 這個SPI來說其適配類就是自動生成的,其實現也很簡單,主要是從RPC的URL配置中獲取到對應的配置,然後再去 ExtensionLoader 中獲取相應的實例.

public class ProxyFactory$Adaptive implements com.alibaba.dubbo.rpc.ProxyFactory { public java.lang.Object getProxy(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.RpcException { if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null"); if (arg0.getUrl() == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null"); com.alibaba.dubbo.common.URL url = arg0.getUrl(); String extName = url.getParameter("proxy", "javassist"); if (extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.ProxyFactory) name from url(" + url.toString() + ") use keys([proxy])"); com.alibaba.dubbo.rpc.ProxyFactory extension = (com.alibaba.dubbo.rpc.ProxyFactory) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.ProxyFactory.class).getExtension(extName); return extension.getProxy(arg0); } public com.alibaba.dubbo.rpc.Invoker getInvoker(java.lang.Object arg0, java.lang.Class arg1, com.alibaba.dubbo.common.URL arg2) throws com.alibaba.dubbo.rpc.RpcException { if (arg2 == null) throw new IllegalArgumentException("url == null"); com.alibaba.dubbo.common.URL url = arg2; String extName = url.getParameter("proxy", "javassist"); if (extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.ProxyFactory) name from url(" + url.toString() + ") use keys([proxy])"); com.alibaba.dubbo.rpc.ProxyFactory extension = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.ProxyFactory.class).getExtension(extName); return extension.getInvoker(arg0, arg1, arg2); }}

根據配置拿到實例

獲取指定名稱實例的方法為 com.alibaba.dubbo.common.extension.ExtensionLoader#getExtension 方法, ExtensionLoader 在加載時會緩存所有的Class,那麼獲取實際上是會從 ConcurrentMap> cachedInstances Map中獲取,利用雙重檢查鎖方式判斷存在實例則直接返回,不存在則實例化然後再保存.上述動態生成的適配類獲取方式就是如此:

ProxyFactory extension = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getExtension(extName);

另外Dubbo這裡實際上有兩層緩存,在創建時還會存入一個全局的 ConcurrentMap, Object> EXTENSION_INSTANCES 中,有點 不明白為什麼這樣做 ,感興趣的可以詳細去看看源碼,如有思路還請告知.

內存中示意圖

Dubbo——擴展機制實現原理

總結

Dubbo利用SPI機制實現了高度的靈活性設計,模塊之間相互解耦,可以根據配置動態修改,提供了最大的靈活性,是一種值得學習的設計方式.文章筆者的理解如果有偏差還請告知,以免誤人子弟.


分享到:


相關文章: