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

 



Java SPI (Service Provider Interface)


應用程序是內聚服務的聚合。雖然應用程序在應用程序編程接口(api)和類方面提供了更廣泛的功能集,但服務提供了對某些特定應用程序功能或特性的訪問。服務定義了功能的接口和檢索實現的方法。

服務是一組衆所周知的接口和(通常是抽象的)類。服務提供者是服務的特定實現。提供者中的類通常實現接口並繼承服務本身中定義的類。服務提供者可以以擴展的形式安裝在Java平臺的實現中,也就是說,jar文件可以放置在任何常用的擴展目錄中。提供程序也可以通過將它們添加到應用程序的類路徑或其他特定於平臺的方法來提供。

服務提供者實現SPI幷包含一個或多個實現或擴展服務類型(子類)的具體類。一個SPI規範可以有多個提供者。爲了促進鬆散耦合和信息隱藏,提供者類通常不是整個提供者本身,而是一個代理,它包含足夠的功能來決定提供者是否能夠滿足特定的請求。

可以通過簡單地添加一個新的Java Archive (JAR)文件來安裝服務提供者,該文件將提供者類保存到應用程序的類路徑中,或者將JAR放置到任何常用的擴展目錄中(jre/lib/ext)。

通過在資源目錄META-INF/services中放置一個提供程序配置文件來識別服務提供者。文件的名稱是服務類型的完全限定二進制名稱。該文件包含具體提供程序類的完全限定二進制名稱列表,每行一個。






插件式架構(Plug-in architecture)



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






Dubbo 的插件式設計


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




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 中定義的擴展 :
adaptive=com.alibaba.dubbo.common.extension.factory.AdaptiveExtensionFactoryspi=com.alibaba.dubbo.common.extension.factory.SpiExtensionFactoryspring=com.alibaba.dubbo.config.spring.extension.SpringExtensionFactory


默認適應擴展 :被 @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 註解缺省指定的擴展。通過這種方式就實現了運行時去適配到對應的擴展。運行時動態生成的適配擴展類代碼 :


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); }}


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); }}


這些代碼都是模板代碼 , 最核心的代碼就只有一行 , 這行代碼是去獲取一個指定名稱的擴展實例對象 :
ExtensionLoader.getExtensionLoader(Xxx.class).getExtension(extName);



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





擴展加載器 ExtensionLoader 


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



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


Class<?> type;
ExtensionFactory objectFactory;
ConcurrentMap<Class<?>, String> cachedNames;
Holder<Map<String, Class<?>>> cachedClasses;
Map<String, Activate> cachedActivates;
Class<?> cachedAdaptiveClass;
ConcurrentMap<String, Holder<Object>> cachedInstances;
String cachedDefaultName;
Holder<Object> cachedAdaptiveInstance;
Throwable createAdaptiveInstanceError;
Set<Class<?>> cachedWrapperClasses;
Map<String, IllegalStateException> exceptions;


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 開放的擴展點 :
com.alibaba.dubbo.cache.CacheFactorycom.alibaba.dubbo.common.compiler.Compilercom.alibaba.dubbo.common.extension.ExtensionFactorycom.alibaba.dubbo.common.logger.LoggerAdaptercom.alibaba.dubbo.common.serialize.Serializationcom.alibaba.dubbo.common.status.StatusCheckercom.alibaba.dubbo.common.store.DataStorecom.alibaba.dubbo.common.threadpool.ThreadPoolcom.alibaba.dubbo.container.Containercom.alibaba.dubbo.container.page.PageHandlercom.alibaba.dubbo.monitor.MonitorFactorycom.alibaba.dubbo.registry.RegistryFactorycom.alibaba.dubbo.remoting.Codec2com.alibaba.dubbo.remoting.Dispatchercom.alibaba.dubbo.remoting.exchange.Exchangercom.alibaba.dubbo.remoting.http.HttpBindercom.alibaba.dubbo.remoting.p2p.Networkercom.alibaba.dubbo.remoting.telnet.TelnetHandlercom.alibaba.dubbo.remoting.Transportercom.alibaba.dubbo.remoting.zookeeper.ZookeeperTransportercom.alibaba.dubbo.rpc.cluster.Clustercom.alibaba.dubbo.rpc.cluster.ConfiguratorFactorycom.alibaba.dubbo.rpc.cluster.LoadBalancecom.alibaba.dubbo.rpc.cluster.Mergercom.alibaba.dubbo.rpc.cluster.RouterFactorycom.alibaba.dubbo.rpc.Filtercom.alibaba.dubbo.rpc.InvokerListenercom.alibaba.dubbo.rpc.Protocolcom.alibaba.dubbo.rpc.protocol.thrift.ClassNameGeneratorcom.alibaba.dubbo.rpc.ProxyFactorycom.alibaba.dubbo.validation.Validation




本文分享自微信公衆號 - 黑帽子技術(SNJYYNJY2020)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章