一文讓你讀懂 Dubbo 中的 SPI 擴展機制

寫在前面

最近接觸了 gRPC 體會到雖然眾多 RPC 框架各有各的特點但是他們提供的特性和功能有很多的相似之處 , 這就說明他們面對同樣的分佈式系統帶來的問題。從 2016 年左右開始接觸到 dubbo ,基本停留在使用的層面,對 dubbo 的設計以及著重要解決的問題都沒有系統的研究過,通過對 dubbo 和其他類似 RPC 產品的系統學習 ,學習分佈式系統中面臨的共同問題以及解決之道。


微內核架構

微內核架構 (Microkernel architecture) 模式也被稱為插件架構 (Plugin architecture) 模式。原本與內核集成在一起的組件會被分離出來,內核提供了特定的接口使得這些組件可以靈活的接入,這些組件在內核的管理下工作,但是這些組件可以獨立的發展、更改(不會對現有系統造成改動),只要符合內核的接口即可。典型的例子比如 , Eclipse , IDEA 。


一文讓你讀懂 Dubbo 中的 SPI 擴展機制


Dubbo 的微內核設計

根據我個人對 Dubbo 微內核設計的理解,以及閱讀源碼後總結。視覺總是最直觀的,可以讓大腦最快速度的有一個最直觀的認識,一開始就一頭深入到源碼的細節中只會讓人迷糊。不理解 Dubbo 的微內核設計架構的話,學習起來會走不少彎路。


一文讓你讀懂 Dubbo 中的 SPI 擴展機制

dubbo 內核對擴展是無感的 , 完全不知道擴展的存在 , 內核代碼中不會出現使用具體擴展的硬編碼。

術語說明 :

SPI : Service Provider Interface 。

擴展點 : 稱 Dubbo 中被 @SPI 註解的 Interface 為一個擴展點。

擴展 : 被 @SPI 註解的 Interface 的實現稱為這個擴展點的一個擴展。


Dubbo SPI 約定

擴展點約定 : 擴展點必須是 Interface 類型 , 必須被 @SPI 註解 , 滿足這兩點才是一個擴展點。

擴展定義約定 : 在 META-INF/services/$擴展點接口的全類名 , META-INF/dubbo/$擴展點接口的全類名 , META-INF/dubbo/internal/$擴展點接口的全類名 , 這些路徑下定義的文件名稱為 $擴展點接口的全類名 , 文件中以鍵值對的方式配置擴展點的擴展實現。例如文件 META-INF/dubbo/internal/com.alibaba.dubbo.common.extension.ExtensionFactory 中定義的擴展 :

<code>adaptive=com.alibaba.dubbo.common.extension.factory.AdaptiveExtensionFactory
spi=com.alibaba.dubbo.common.extension.factory.SpiExtensionFactory
spring=com.alibaba.dubbo.config.spring.extension.SpringExtensionFactory/<code>

默認適應擴展 : 被 @SPI("abc") 註解的 Interface , 那麼這個擴展點的缺省適應擴展就是 SPI 配置文件中 key 為 "abc" 的擴展。如果存在被 @Adaptive 註解在類上的擴展點接口實現 ,那麼這個類就作為擴展點的缺省適應擴展, 一個擴展點只能有一個缺省適應擴展也就是說多個擴展中只能有一個在類上被 @Adaptive 註解,如果有多個 dubbo 會拋出 IllegalStateException("More than 1 adaptive class found : ")。


@SPI 、@Adaptive 、@Activate 作用

@SPI (註解在類上) : @SPI 註解標識了接口是一個擴展點 , 屬性 value 用來指定默認適配擴展點的名稱。

@Activate (註解在類型和方法上) : @Activate 註解在擴展點的實現類上 ,表示了一個擴展類被獲取到的的條件,符合條件就被獲取,不符合條件就不獲取 ,根據 @Activate 中的 group 、 value 屬性來過濾 。具體參考 ExtensionLoader 中的 getActivateExtension 函數。

@Adaptive (註解在類型和方法上) : @Adaptive 註解在類上 , 這個類就是缺省的適配擴展。@Adaptive 註解在擴展點 Interface 的方法上時 , dubbo 動態的生成一個這個擴展點的適配擴展類(生成代碼 ,動態編譯實例化 Class ),名稱為 擴展點 Interface 的簡單類名 + $Adaptive ,例如 : ProxyFactory$Adpative 。這麼做的目的是為了在運行時去適配不同的擴展實例 , 在運行時通過傳入的 URL 類型的參數或者內部含有獲取 URL 方法的參數 ,從 URL 中獲取到要使用的擴展類的名稱 ,再去根據名稱加載對應的擴展實例 ,用這個擴展實例對象調用相同的方法 。如果運行時沒有適配到運行的擴展實例 , 那麼就使用 @SPI 註解缺省指定的擴展。通過這種方式就實現了運行時去適配到對應的擴展。


運行時動態生成的適配擴展類代碼 :

<code>
package com.alibaba.dubbo.rpc;

import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
public void destroy() {
throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}

public int getDefaultPort() {
throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}

public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws java.lang.Class {
if (arg1 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg1;

// 從 URL 中獲取擴展名稱 , "dubbo" 是從 ExtensionLoader 對象中的 cachedDefaultName
// 屬性獲取到的 , cachedDefaultName 是擴展點上 @SPI 註解中 value 屬性指定的
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
if (extName == null)
throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");

// 通過擴展名稱獲取擴展實例對象 , 調用擴展實例對象的相同方法
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.refer(arg0, arg1);
}

public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.Invoker {
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();

// 從 URL 中獲取擴展名稱

String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
if (extName == null)
throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");

// 通過擴展名稱獲取擴展實例對象 , 調用擴展實例對象的相同方法
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.export(arg0);
}
}/<code>
<code>package com.alibaba.dubbo.rpc;

import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class ProxyFactory$Adpative implements com.alibaba.dubbo.rpc.ProxyFactory {
public java.lang.Object getProxy(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.Invoker {
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();

// 從 URL 中獲取擴展名稱
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 java.lang.Object {
if (arg2 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg2;

// 從 URL 中獲取擴展名稱
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.getInvoker(arg0, arg1, arg2);
}
}/<code>

這些代碼都是模板代碼 , 最核心的代碼就只有一行 , 這行代碼是去獲取一個指定名稱的擴展實例對象 :

<code>ExtensionLoader.getExtensionLoader(Xxx.class).getExtension(extName);/<code>

在使用 dubbo 生成的源碼時要注意 , 它生成的代碼是有錯誤的, 比如 Protocol$Adpative 類的中的 refer 和 export 方法簽名 throws 的不是 Protocol 接口中方法定義拋出的 RpcException , 而是 Class ,和 Invoker 。 這個問題存在於 dubbo 2.5.4 之前的版本中,在 2.5.4 版本中修復了。

擴展加載器 ExtensionLoader

擴展加載器絕對是一個核心組件了 ,它控制著 dubbo 內部所有擴展點的初始化、加載擴展的過程。這個類的源碼是很有必要深入學習的。從 Dubbo 內核設計簡圖可以看到,現在的學習還沒有接觸到 dubbo 的內核。


一文讓你讀懂 Dubbo 中的 SPI 擴展機制

ExtensionLoader 中會存儲兩個靜態屬性 , EXTENSION_LOADERS 保存了內核開放的擴展點對應的 ExtensionLoader 實例對象 (說明了一種擴展點有一個對應的 ExtensionLoader 對象)。EXTENSION_INSTANCES 保存了擴展類型 (Class) 和擴展類型的實例對象。

ExtensionLoader 對象中的屬性 :

<code>Class> type;

ExtensionFactory objectFactory;

ConcurrentMap<class>, String> cachedNames;

Holder>> cachedClasses;

Map<string> cachedActivates;

Class> cachedAdaptiveClass;

ConcurrentMap<string>> cachedInstances;

String cachedDefaultName;

Holder<object> cachedAdaptiveInstance;

Throwable createAdaptiveInstanceError;

Set<class>> cachedWrapperClasses;

Map<string> exceptions;/<string>/<class>/<object>/<string>/<string>
/<class>/<code>

type : 被 @SPI 註解的 Interface , 也就是擴展點。

objectFactory : 擴展工廠,可以從中獲取到擴展類型實例對象 ,缺省為 AdaptiveExtensionFactory。

cachedNames : 保存不滿足裝飾模式(不存在只有一個參數,並且參數是擴展點類型實例對象的構造函數)的擴展的名稱。

cachedClasses : 保存不滿足裝飾模式的擴展的 Class 實例 , 擴展的名稱作為 key , Class 實例作為 value。

cachedActivates : 保存不滿足裝飾模式 , 被 @Activate 註解的擴展的 Class 實例。

cachedAdaptiveClass : 被 @Adpative 註解的擴展的 Class 實例 。

cachedInstances : 保存擴展的名稱和實例對象 , 擴展名稱為 key , 擴展實例為 value。

cachedDefaultName : 擴展點上 @SPI 註解指定的缺省適配擴展。

createAdaptiveInstanceError : 創建適配擴展實例過程中拋出的異常。

cachedWrapperClasses : 滿足裝飾模式的擴展的 Class 實例。

exceptions : 保存在加載擴展點配置文件時,加載擴展點過程中拋出的異常 , key 是當前讀取的擴展點配置文件的一行 , value 是拋出的異常。

附: dubbo 開放的擴展點

<code>com.alibaba.dubbo.cache.CacheFactory
com.alibaba.dubbo.common.compiler.Compiler
com.alibaba.dubbo.common.extension.ExtensionFactory
com.alibaba.dubbo.common.logger.LoggerAdapter
com.alibaba.dubbo.common.serialize.Serialization
com.alibaba.dubbo.common.status.StatusChecker
com.alibaba.dubbo.common.store.DataStore
com.alibaba.dubbo.common.threadpool.ThreadPool
com.alibaba.dubbo.container.Container
com.alibaba.dubbo.container.page.PageHandler
com.alibaba.dubbo.monitor.MonitorFactory
com.alibaba.dubbo.registry.RegistryFactory
com.alibaba.dubbo.remoting.Codec2
com.alibaba.dubbo.remoting.Dispatcher
com.alibaba.dubbo.remoting.exchange.Exchanger
com.alibaba.dubbo.remoting.http.HttpBinder
com.alibaba.dubbo.remoting.p2p.Networker
com.alibaba.dubbo.remoting.telnet.TelnetHandler
com.alibaba.dubbo.remoting.Transporter
com.alibaba.dubbo.remoting.zookeeper.ZookeeperTransporter
com.alibaba.dubbo.rpc.cluster.Cluster
com.alibaba.dubbo.rpc.cluster.ConfiguratorFactory
com.alibaba.dubbo.rpc.cluster.LoadBalance
com.alibaba.dubbo.rpc.cluster.Merger
com.alibaba.dubbo.rpc.cluster.RouterFactory
com.alibaba.dubbo.rpc.Filter
com.alibaba.dubbo.rpc.InvokerListener
com.alibaba.dubbo.rpc.Protocol
com.alibaba.dubbo.rpc.protocol.thrift.ClassNameGenerator
com.alibaba.dubbo.rpc.ProxyFactory
com.alibaba.dubbo.validation.Validation/<code>


分享到:


相關文章: