1. 程式人生 > >Dubbo/Dubbox的服務暴露(二)-擴充套件點機制

Dubbo/Dubbox的服務暴露(二)-擴充套件點機制

上文書留的疑問,這兩句到底在幹啥

                        Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));

                        Exporter<?> exporter = protocol.export(invoker);
                        exporters.add
(exporter);

首先我們去看proxyFactory.getInvoker 方法,proxyFactory是一個全域性變數,點開看一下。保持了dubbo程式碼一貫清爽的風格,又沒有文件註釋。乏開心……

private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();

dubbo原始碼中到處都是 ExtensionLoader.getExtensionLoader(XXXX.class),為避免影響閱讀,先要了解下這個類的用途,點進ExtensionLoader這個類去看看大佬們做了些啥,點進去一看,大佬們留註釋了。喜極而泣。

package com.alibaba.dubbo.common.extension
/**
 * Dubbo使用的擴充套件點獲取。<p>
 * <ul>
 * <li>自動注入關聯擴充套件點。</li>
 * <li>自動Wrap上擴充套件點的Wrap類。</li>
 * <li>預設獲得的的擴充套件點是一個Adaptive Instance。
 * </ul>
 * 
 * @see <a href="http://java.sun.com/j2se/1.5.0/docs/guide/jar/jar.html#Service%20Provider">JDK5.0的自動發現機制實現</a>
 * 
 * @author
william.liangf * @author ding.lid * * @see com.alibaba.dubbo.common.extension.SPI * @see com.alibaba.dubbo.common.extension.Adaptive * @see com.alibaba.dubbo.common.extension.Activate */
public class ExtensionLoader<T> { //...............// }

通過dubbo中少有的文件註釋,可以提煉幾個關鍵字SPI,Adaptive,Activate
其中SPI,我之前轉過一篇文章《Java spi機制淺談》
文章有說

java spi的具體約定如下 :
當服務的提供者,提供了服務介面的一種實現之後,在jar包的META-INF/services/目錄裡同時建立一個以服務介面命名的檔案。該檔案裡就是實現該服務介面的具體實現類。而當外部程式裝配這個模組的時候,就能通過該jar包META-INF/services/裡的配置檔案找到具體的實現類名,並裝載例項化,完成模組的注入。
基於這樣一個約定就能很好的找到服務介面的實現類,而不需要再程式碼裡制定。
jdk提供服務實現查詢的一個工具類:java.util.ServiceLoader

等等ServiceLoader和ExtensionLoader命名頗像啊,ExtensionLoader.getExtensionLoader(XXXX.class)與ServiceLoader.load(Search.class); 使用方式也頗像啊,點進去看看getExtensionLoader方法都做了些啥,以上文protocol 為例

 private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

該protocol屬性的生成過程如下

@SuppressWarnings("unchecked")
    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!");
        }
        // 檢查該介面上是否使用@SPI註解
        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是執行緒安全的map物件(ConcurrentMap),典型的單例,避免針對同一介面生成多個ExtensionLoader例項
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

ExtensionLoader.getExtensionLoader(Protocol.class) 會返回一個ExtensionLoader 的一個例項,後續還通過這個ExtensionLoader 呼叫了getAdaptiveExtension() 方法?那好繼續點選去看。這年後英語在程式設計領域當道,真是妨礙漢語發展,坐等廣電要求程式設計領域使用漢語程式設計[滑稽],adaptive是個啥意思呢?ps:這裡強烈吐槽百度的翻譯結果和例句。

adaptive指有適應性的;傾向於適應能力的,為適應能力而設計的,合適於適應能力的或具有適應能力的。

 @SuppressWarnings("unchecked") 
    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);
                        }
                    }
                }
            }
            else {
                throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
            }
        }

        return (T) instance;
    }

很好createAdaptiveExtension(),又是一個unchecked的方法。

 @SuppressWarnings("unchecked")
    private T createAdaptiveExtension() {
        try {
            //先呼叫 getAdaptiveExtensionClass().newInstance() 方法,那麼我們又要了解getAdaptiveExtensionClass() 方法
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can not create adaptive extenstion " + type + ", cause: " + e.getMessage(), e);
        }
    }

getAdaptiveExtensionClass() 實現如下。

private Class<?> getAdaptiveExtensionClass() {
        //該方法除了一堆快取操作,內部會執行loadExtensionClasses();方法
        getExtensionClasses();
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

以上程式碼我們知道,getAdaptiveExtensionClass() 內部會呼叫loadExtensionClasses()方法

// 此方法已經getExtensionClasses方法同步過。
    private Map<String, Class<?>> loadExtensionClasses() {
        // 拿到@SPI 註解
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if(defaultAnnotation != null) { //如果找到該註解
            String value = defaultAnnotation.value(); //獲得擴充套件點名
            if(value != null && (value = value.trim()).length() > 0) { //如果指定了擴充套件點名
                //NAME_SEPARATOR 是一個這樣的正則表示式 Pattern.compile("\\s*[,]+\\s*") 作用可想而知
                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));
                }
                if(names.length == 1) cachedDefaultName = names[0];
            }
        }

        Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
         //依次載入三個路徑下的配置檔案
        loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
        loadFile(extensionClasses, DUBBO_DIRECTORY);
        loadFile(extensionClasses, SERVICES_DIRECTORY);
        return extensionClasses;
    }

上文中loadFile(extensionClasses, XXXXX);方法中使用的幾個常量如下。

 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/";

以 loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY); 為例,看一下META-INF/dubbo/internal下的檔案

這裡寫圖片描述

以 META-INF\dubbo\internal\com.alibaba.dubbo.rpc.Protocol 檔案為例,其檔案內容:

registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol

可見,檔案內容按key=value的格式編寫,key為擴充套件點名稱,value為擴充套件點邏輯類名。loadFile的處理邏輯如下,遍歷指定目錄的檔案,按行讀取檔案內容,判斷=號右邊的類是否為當前擴充套件點的實現,如果是將=號左邊的作為key,右邊的類物件作為value,儲存在引數的map中
ps:還有一種配置方式,但已不使用,祥見com.alibaba.dubbo.common.extension.SPI 類的文件註釋,loadFile方法依舊相容老的配置方式
dubbo的擴充套件點還有一個誤區,即其支援包裝類的配置,包裝類需實現擴充套件點介面,並含有一個以當前擴充套件點介面為唯一引數的構造方法。一個典型的包裝類配置如下:

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
/**
 * ListenerProtocol
 * 
 * @author william.liangf
 */
public class ProtocolFilterWrapper implements Protocol {

    private final Protocol protocol;
    // 注意
    public ProtocolFilterWrapper(Protocol protocol){
        if (protocol == null) {
            throw new IllegalArgumentException("protocol == null");
        }
        this.protocol = protocol;
    }
    /*******/
}

然後我們繼續回到getAdaptiveExtensionClass()方法,其在getExtensionClasses();方法後,繼續呼叫createAdaptiveExtensionClass();方法,這個方法比較複雜,只做大概總結。
1.在擴充套件點所在包名下建立一個類
2.類名規則"public class " + type.getSimpleName() + "$Adpative" + " implements " + type.getCanonicalName()(反射相關知識)
3.對擴充套件點介面的每個方法做一個封裝,這裡分為兩個處理分支
3.1 方法上沒有@Adaptive 註解,以com.alibaba.dubbo.rpc.Protocol 的void destroy(); 方法為例,預設實現為以下,即會丟擲UnsupportedOperationException異常。

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

3.2 方法上有 @Adaptive 註解,這個需要注意啦
3.2.1 會判斷方法上是不是有型別為com.alibaba.dubbo.common.URL 型別的引數,如果沒有對應型別的引數,則依次判斷引數的類中是否有型別為com.alibaba.dubbo.common.URL的屬性,如果都沒有則方法實現丟擲IllegalStateException異常,如果有與URL有關的引數,則已當前擴充套件點名為引數,呼叫 String extName = url.getParameter(擴充套件點名,預設值); 然後呼叫ExtensionLoader.getExtensionLoader(擴充套件點類.class).getExtension(extName); 一個典型的樣例如下(為保持一致,這裡依舊使用protocol的擴充套件點,protocol有特殊處理,但不影響閱讀理解):

public com.alibaba.dubbo.rpc.Exporter export(
        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.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);
    }

最後呼叫的getExtension(extName) 來找到擴充套件點的具體實現,其實看到這感覺已經不需要繼續分析getExtension方法了,因為我們上邊看到dubbo通過loadFile方法載入了當前擴充套件點的實現類的對應關係,
在上文所例的檔案中有這樣一行,

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol

而我們通過上邊樣例中的String extName = ((url.getProtocol() == null) ? "dubbo": url.getProtocol());取到的extName 恰恰為dubbo,因為看到這裡其實整個擴充套件點的載入機制已經很清晰了,getExtension(String name) 無非是返回指定名字的擴充套件類例項,這裡抄一個道友的對該方法的簡單描述:

getExtension(String name)方法:返回指定名字的擴充套件類例項,
1)如果指定名字的擴充套件類不存在,則拋異常。
2)如果指定的名字為“true”,則返回預設的實現類例項,即name= cachedDefaultName;
3)從ExtensionLoader.cachedInstances:ConcurrentHashMap變數中獲取該name的例項;
4)若Map中沒有該name的例項,則呼叫createExtension方法建立該例項,並儲存到快取中。

注意:如果當前擴充套件點含有上文所說的包裝類,則會初始化擴充套件點類的基礎上,返回包裝類的物件。一個典型的情況是,Protocol的適應性擴充套件點為dubbo,該方法會返回包裝類com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper的例項
關於擴充套件點的小結:
所謂適應性擴充套件點的實現,其實是與URL息息相關的。
1.要求方法上必須有Adaptive 註解
2.方法上必須有與com.alibaba.dubbo.common.URL 有關的引數(包含屬性,或直接引數)
3.URL的引數中必須有對應的擴充套件點名稱的值或者擴充套件點有指定預設值,否則會丟擲IllegalStateException 異常。
4.可以通過在介面上的SPI註解上定義擴充套件點的預設值(extName),例:@SPI(FailoverCluster.NAME)
其他參考Adaptive 的文件註釋(贊):

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {

    /**
     * 從{@link URL}的Key名,對應的Value作為要Adapt成的Extension名。
     * <p>
     * 如果{@link URL}這些Key都沒有Value,使用 用 預設的擴充套件(在介面的{@link SPI}中設定的值)。<br>
     * 比如,<code>String[] {"key1", "key2"}</code>,表示
     * <ol>
     * <li>先在URL上找key1的Value作為要Adapt成的Extension名;
     * <li>key1沒有Value,則使用key2的Value作為要Adapt成的Extension名。
     * <li>key2沒有Value,使用預設的擴充套件。
     * <li>如果沒有設定預設擴充套件,則方法呼叫會丟擲{@link IllegalStateException}。
     * </ol>
     * <p>
     * 如果不設定則預設使用Extension介面類名的點分隔小寫字串。<br>
     * 即對於Extension介面{@code com.alibaba.dubbo.xxx.YyyInvokerWrapper}的預設值為<code>String[] {"yyy.invoker.wrapper"}</code>
     * 
     * @see SPI#value()
     */
    String[] value() default {};

}