1. 程式人生 > 其它 >Dubbo解析之SPI和自適應擴充套件

Dubbo解析之SPI和自適應擴充套件

在繼續深入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物件,在這裡我們把它親切的稱為:擴充套件點載入器。 我們先看下它的屬性:
public
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>>(); }
ExtensionLoader會把不同的擴充套件點配置和實現都快取起來。同時,Dubbo在官網上也給了我們提醒:擴充套件點使用單一例項載入(請確保擴充套件實現的執行緒安全性),快取在 ExtensionLoader中。下面我們看幾個重點方法。

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類,完成相應的邏輯處理。