Dubbo解析之SPI和自適應擴充套件
阿新 • • 發佈:2021-12-06
在繼續深入Dubbo之前,必須先要明白Dubbo中的SPI機制。
一、背景
1、來源
Dubbo 的擴充套件點載入從 JDK 標準的 SPI (Service Provider Interface) 擴充套件點發現機制加強而來。但還有所不同,它改進了JDK標準的 SPI的以下問題:- JDK 標準的 SPI 會一次性例項化擴充套件點所有實現,如果有擴充套件實現初始化很耗時,但如果沒用上也載入,會很浪費資源。
- 如果擴充套件點載入失敗,連擴充套件點的名稱都拿不到。比如:JDK 標準的 ScriptEngine,通過 getName() 獲取指令碼型別的名稱,但如果 RubyScriptEngine 因為所依賴的 jruby.jar 不存在,導致 RubyScriptEngine 類載入失敗,這個失敗原因被吃掉了,和 ruby 對應不起來,當用戶執行 ruby 指令碼時,會報不支援 ruby,而不是真正失敗的原因。
- 增加了對擴充套件點 IoC 和 AOP 的支援,一個擴充套件點可以直接 setter 注入其它擴充套件點。
2、約定
在擴充套件類的 jar 包內,放置擴充套件點配置檔案 META-INF/dubbo/介面全限定名,內容為:配置名=擴充套件實現類全限定名,多個實現類用換行符分隔。3、配置檔案
Dubbo SPI 所需的配置檔案需放置在 META-INF/dubbo 路徑下,幾乎所有的功能都有擴充套件點實現。 以Protocol介面為例,它裡面有很多實現。二、Dubbo SPI
通過上圖我們可以看到,Dubbo SPI和JDK SPI配置的不同,在Dubbo SPI中可以通過鍵值對的方式進行配置,這樣就可以按需載入指定的實現類。 Dubbo SPI的相關邏輯都被封裝到ExtensionLoader類中,通過ExtensionLoader我們可以載入指定的實現類,一個擴充套件介面就對應一個ExtensionLoader物件,在這裡我們把它親切的稱為:擴充套件點載入器。 我們先看下它的屬性:publicExtensionLoader會把不同的擴充套件點配置和實現都快取起來。同時,Dubbo在官網上也給了我們提醒:擴充套件點使用單一例項載入(請確保擴充套件實現的執行緒安全性),快取在 ExtensionLoader中。下面我們看幾個重點方法。class ExtensionLoader<T> { //擴充套件點配置檔案的路徑,可以從3個地方載入到擴充套件點配置檔案 private static final String SERVICES_DIRECTORY = "META-INF/services/"; private static final String DUBBO_DIRECTORY = "META-INF/dubbo/"; private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";//擴充套件點載入器的集合 private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>(); //擴充套件點實現的集合 private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>(); //擴充套件點名稱和實現的對映快取 private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<Class<?>, String>(); //拓展點實現類集合快取 private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<Map<String, Class<?>>>(); //擴充套件點名稱和@Activate的對映快取 private final Map<String, Activate> cachedActivates = new ConcurrentHashMap<String, Activate>(); //擴充套件點實現的快取 private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<String, Holder<Object>>(); }
1、獲取擴充套件點載入器
我們首先通過ExtensionLoader.getExtensionLoader() 方法獲取一個 ExtensionLoader 例項,它就是擴充套件點載入器。然後再通過 ExtensionLoader 的 getExtension 方法獲取拓展類物件。這其中,getExtensionLoader 方法用於從快取中獲取與拓展類對應的 ExtensionLoader,若快取未命中,則建立一個新的例項。public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) { if (type == null) throw new IllegalArgumentException("Extension type == null"); if (!type.isInterface()) { throw new IllegalArgumentException("Extension type(" + type + ") is not interface!"); } if (!withExtensionAnnotation(type)) { throw new IllegalArgumentException("Extension type(" + type + ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!"); } ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); if (loader == null) { EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type)); loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); } return loader; }比如可以通過下面這樣,來獲取Protocol介面的ExtensionLoader例項:
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
就可以拿到擴充套件點載入器的物件例項:
com.alibaba.dubbo.common.extension.ExtensionLoader[com.alibaba.dubbo.rpc.Protocol]
2、獲取擴充套件類物件
上一步我們已經拿到載入器,然後可以根據載入器例項,通過擴充套件點的名稱獲取擴充套件類物件。public T getExtension(String name) { //校驗擴充套件點名稱的合法性 if (name == null || name.length() == 0) throw new IllegalArgumentException("Extension name == null"); // 獲取預設的拓展實現類 if ("true".equals(name)) { return getDefaultExtension(); } //用於持有目標物件 Holder<Object> holder = cachedInstances.get(name); if (holder == null) { cachedInstances.putIfAbsent(name, new Holder<Object>()); holder = cachedInstances.get(name); } Object instance = holder.get(); if (instance == null) { synchronized (holder) { instance = holder.get(); if (instance == null) { instance = createExtension(name); holder.set(instance); } } } return (T) instance; }它先嚐試從快取中獲取,未命中則建立擴充套件物件。那麼它的建立過程是怎樣的呢?
private T createExtension(String name) { //從配置檔案中獲取所有的擴充套件類,Map資料結構 //然後根據名稱獲取對應的擴充套件類 Class<?> clazz = getExtensionClasses().get(name); if (clazz == null) { throw findException(name); } try { //通過反射建立例項,然後放入快取 T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } //注入依賴 injectExtension(instance); Set<Class<?>> wrapperClasses = cachedWrapperClasses; if (wrapperClasses != null && !wrapperClasses.isEmpty()) { // 包裝為Wrapper例項 for (Class<?> wrapperClass : wrapperClasses) { instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; } catch (Throwable t) { throw new IllegalStateException("Extension instance(name: " + name + ", class: " + type + ") could not be instantiated: " + t.getMessage(), t); } }這裡的重點有兩個,依賴注入和Wrapper包裝類,它們是Dubbo中IOC 與 AOP 的具體實現。
2.1、依賴注入
向拓展物件中注入依賴,它會獲取類的所有方法。判斷方法是否以 set 開頭,且方法僅有一個引數,且方法訪問級別為 public,就通過反射設定屬性值。所以說,Dubbo中的IOC僅支援以setter方式注入。private T injectExtension(T instance) { try { if (objectFactory != null) { for (Method method : instance.getClass().getMethods()) { if (method.getName().startsWith("set") && method.getParameterTypes().length == 1 && Modifier.isPublic(method.getModifiers())) { Class<?> pt = method.getParameterTypes()[0]; try { String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : ""; Object object = objectFactory.getExtension(pt, property); if (object != null) { method.invoke(instance, object); } } catch (Exception e) { logger.error("fail to inject via method " + method.getName() + " of interface " + type.getName() + ": " + e.getMessage(), e); } } } } } catch (Exception e) { logger.error(e.getMessage(), e); } return instance; }
2.2、Wrapper
它會將當前 instance 作為引數傳給 Wrapper 的構造方法,並通過反射建立 Wrapper 例項。 然後向 Wrapper 例項中注入依賴,最後將 Wrapper 例項再次賦值給 instance 變數。說起來可能比較繞,我們直接看下它最後生成的物件就明白了。 我們以DubboProtocol為例,它包裝後的物件為: 綜上所述,如果我們獲取一個擴充套件類物件,最後拿到的就是這個Wrapper類的例項。 就像這樣:ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class); Protocol extension = extensionLoader.getExtension("dubbo"); System.out.println(extension);輸出為:com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper@4cdf35a9
3、獲取所有的擴充套件類
在通過名稱獲取擴充套件類物件之前,首先需要根據配置檔案解析出所有的擴充套件類,它是一個擴充套件點名稱和擴充套件類的對映表Map<string, class<?="">>。首先,還是從快取中cachedClasses獲取,如果沒有就呼叫loadExtensionClasses從配置檔案中載入。配置檔案有三個路徑:- META-INF/services/
- META-INF/dubbo/
- META-INF/dubbo/internal/
private Map<String, Class<?>> getExtensionClasses() { //從快取中獲取 Map<String, Class<?>> classes = cachedClasses.get(); if (classes == null) { synchronized (cachedClasses) { classes = cachedClasses.get(); if (classes == null) { //載入擴充套件類 classes = loadExtensionClasses(); cachedClasses.set(classes); } } } return classes; }如果沒有,就呼叫loadExtensionClasses從配置檔案中讀取。
private Map<String, Class<?>> loadExtensionClasses() { //獲取 SPI 註解,這裡的 type 變數是在呼叫 getExtensionLoader 方法時傳入的 final SPI defaultAnnotation = type.getAnnotation(SPI.class); if (defaultAnnotation != null) { String value = defaultAnnotation.value(); if ((value = value.trim()).length() > 0) { String[] names = NAME_SEPARATOR.split(value); if (names.length > 1) { throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()+ ": " + Arrays.toString(names)); } //設定預設的副檔名稱,參考getDefaultExtension 方法 //如果名稱為true,就是呼叫預設擴贊類 if (names.length == 1) cachedDefaultName = names[0]; } } //載入指定路徑的配置檔案 Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>(); loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY); loadDirectory(extensionClasses, DUBBO_DIRECTORY); loadDirectory(extensionClasses, SERVICES_DIRECTORY); return extensionClasses; }以Protocol介面為例,獲取到的實現類集合如下,我們就可以根據名稱載入具體的擴充套件類物件。
{ registry=class com.alibaba.dubbo.registry.integration.RegistryProtocol injvm=class com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol thrift=class com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol mock=class com.alibaba.dubbo.rpc.support.MockProtocol dubbo=class com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol http=class com.alibaba.dubbo.rpc.protocol.http.HttpProtocol redis=class com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol rmi=class com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol }
三、自適應擴充套件機制
在Dubbo中,很多拓展都是通過 SPI 機制進行載入的,比如 Protocol、Cluster、LoadBalance 等。這些擴充套件並非在框架啟動階段就被載入,而是在擴充套件方法被呼叫的時候,根據URL物件引數進行載入。 那麼,Dubbo就是通過自適應擴充套件機制來解決這個問題。 自適應拓展機制的實現邏輯是這樣的: 首先 Dubbo 會為拓展介面生成具有代理功能的程式碼。然後通過 javassist 或 jdk 編譯這段程式碼,得到 Class 類。最後再通過反射建立代理類,在代理類中,就可以通過URL物件的引數來確定到底呼叫哪個實現類。1、Adaptive註解
在開始之前,我們有必要先看一下與自適應拓展息息相關的一個註解,即 Adaptive 註解。@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Adaptive { String[] value() default {}; }從上面的程式碼中可知,Adaptive 可註解在類或方法上。
- 標註在類上
Dubbo 不會為該類生成代理類。
- 標註在方法上
Dubbo 則會為該方法生成代理邏輯,表示當前方法需要根據 引數URL 呼叫對應的擴充套件點實現。
2、獲取自適應拓展類
getAdaptiveExtension 方法是獲取自適應拓展的入口方法。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) { createAdaptiveInstanceError = t; throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t); } } } } } return (T) instance; }getAdaptiveExtension方法首先會檢查快取,快取未命中,則呼叫 createAdaptiveExtension方法建立自適應拓展。
private T createAdaptiveExtension() { try { return injectExtension((T) getAdaptiveExtensionClass().newInstance()); } catch (Exception e) { throw new IllegalStateException(" Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e); } }這裡的程式碼較少,呼叫 getAdaptiveExtensionClass方法獲取自適應拓展 Class 物件,然後通過反射例項化,最後呼叫injectExtension方法向拓展例項中注入依賴。 獲取自適應擴充套件類過程如下:
private Class<?> getAdaptiveExtensionClass() { //獲取當前介面的所有實現類 //如果某個實現類標註了@Adaptive,此時cachedAdaptiveClass不為空 getExtensionClasses(); if (cachedAdaptiveClass != null) { return cachedAdaptiveClass; } //以上條件不成立,就建立自適應拓展類 return cachedAdaptiveClass = createAdaptiveExtensionClass(); }在上面方法中,它會先獲取當前介面的所有實現類,如果某個實現類標註了@Adaptive,那麼該類就被賦值給cachedAdaptiveClass變數並返回。如果沒有,就呼叫createAdaptiveExtensionClass建立自適應拓展類。
3、建立自適應拓展類
createAdaptiveExtensionClass方法用於生成自適應拓展類,該方法首先會生成自適應拓展類的原始碼,然後通過 Compiler 例項(Dubbo 預設使用 javassist 作為編譯器)編譯原始碼,得到代理類 Class 例項。private Class<?> createAdaptiveExtensionClass() { //構建自適應拓展程式碼 String code = createAdaptiveExtensionClassCode(); ClassLoader classLoader = findClassLoader(); // 獲取編譯器實現類 這個Dubbo預設是採用javassist Compiler compiler =ExtensionLoader.getExtensionLoader(Compiler.class).getAdaptiveExtension(); //編譯程式碼,返回類例項的物件 return compiler.compile(code, classLoader); }在生成自適應擴充套件類之前,Dubbo會檢查介面方法是否包含@Adaptive。如果方法上都沒有此註解,就要丟擲異常。
if (!hasAdaptiveAnnotation){ throw new IllegalStateException( "No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!"); }我們還是以Protocol介面為例,它的export()和refer()方法,都標註為@Adaptive。destroy和 getDefaultPort未標註 @Adaptive註解。Dubbo 不會為沒有標註 Adaptive 註解的方法生成代理邏輯,對於該種類型的方法,僅會生成一句丟擲異常的程式碼。
package com.alibaba.dubbo.rpc; import com.alibaba.dubbo.common.URL; import com.alibaba.dubbo.common.extension.Adaptive; import com.alibaba.dubbo.common.extension.SPI; @SPI("dubbo") public interface Protocol { int getDefaultPort(); @Adaptive <T> Exporter<T> export(Invoker<T> invoker) throws RpcException; @Adaptive <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; void destroy(); }所以說當我們呼叫這兩個方法的時候,會先拿到URL物件中的協議名稱,再根據名稱找到具體的擴充套件點實現類,然後去呼叫。下面是生成自適應擴充套件類例項的原始碼:
package com.viewscenes.netsupervisor.adaptive; import com.alibaba.dubbo.common.URL; import com.alibaba.dubbo.common.extension.ExtensionLoader; import com.alibaba.dubbo.rpc.Exporter; import com.alibaba.dubbo.rpc.Invoker; import com.alibaba.dubbo.rpc.Protocol; import com.alibaba.dubbo.rpc.RpcException; public class Protocol$Adaptive implements Protocol { public void destroy() { throw new UnsupportedOperationException( "method public abstract void Protocol.destroy() of interface Protocol is not adaptive method!"); } public int getDefaultPort() { throw new UnsupportedOperationException( "method public abstract int Protocol.getDefaultPort() of interface Protocol is not adaptive method!"); } public Exporter export(Invoker invoker)throws RpcException { if (invoker == null) { throw new IllegalArgumentException("Invoker argument == null"); } if (invoker.getUrl() == null) { throw new IllegalArgumentException("Invoker argument getUrl() == null"); } URL url = invoker.getUrl(); String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); if (extName == null) { throw new IllegalStateException("Fail to get extension(Protocol) name from url(" + url.toString() + ") use keys([protocol])"); } Protocol extension = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName); return extension.export(invoker); } public Invoker refer(Class clazz,URL ur)throws RpcException { if (ur == null) { throw new IllegalArgumentException("url == null"); } URL url = ur; String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); if (extName == null) { throw new IllegalStateException("Fail to get extension(Protocol) name from url("+ url.toString() + ") use keys([protocol])"); } Protocol extension = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName); return extension.refer(clazz, url); } }綜上所述,當我們獲取某個介面的自適應擴充套件類,實際就是一個Adaptive類例項。
ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class); Protocol adaptiveExtension = extensionLoader.getAdaptiveExtension(); System.out.println(adaptiveExtension);輸出為: com.alibaba.dubbo.rpc.Protocol$Adaptive@47f6473
四、例項
看完以上流程之後,如果想寫一套自己的邏輯替換Dubbo中的流程,就變得很簡單。Dubbo預設使用dubbo協議來暴露服務。可以搞一個自定義的協議來替換它。1、實現類
首先,我們建立一個MyProtocol類,它實現Protocol介面。package com.viewscenes.netsupervisor.protocol; import com.alibaba.dubbo.common.URL; import com.alibaba.dubbo.rpc.Exporter; import com.alibaba.dubbo.rpc.Invoker; import com.alibaba.dubbo.rpc.Protocol; import com.alibaba.dubbo.rpc.RpcException; import com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol; public class MyProtocol extends DubboProtocol implements Protocol{ public int getDefaultPort() { return 28080; } public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException { URL url = invoker.getUrl(); System.out.println("自定義協議,進行服務暴露:"+url); return super.export(invoker); } public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException { return super.refer(type, url); } public void destroy() {} }
2、擴充套件點配置檔案
然後,在自己的專案中META-INF/services建立com.alibaba.dubbo.rpc.Protocol檔案,檔案內容為:myProtocol=com.viewscenes.netsupervisor.protocol.MyProtocol
3、修改Dubbo配置檔案
最後修改生產者端的配置檔案:<!-- 用自定義協議在20880埠暴露服務 --> <dubbo:protocol name="myProtocol" port="20880"/>這樣在啟動生產者端專案的時候,Dubbo在進行服務暴露的過程中,就會呼叫到我們自定義的MyProtocol類,完成相應的邏輯處理。