Dubbo SPI機制(下):自激活擴展與自適應擴展的加載

在上一篇文章中,我們瞭解了Dubbo中一個普通的擴展類是如何加載的。今天,我們繼續Dubbo SPI的旅程,來看一看兩個特殊的擴展:自激活擴展和自適應擴展。

自激活擴展

什麼是自激活擴展?

如果一個擴展點有很多擴展實現,而我們需要根據不同的條件參數來動態選擇可以使用的擴展時(可能需要同時使用多個擴展),就會使用到自激活擴展了。

這樣說你可能還是不太清楚,我們舉個例子:
你應該或多或少都聽過Filter的概念吧?衆多的語言概念中都有過濾器的概念,Dubbo中也有。通過各種各樣的過濾器能夠幫助我們實現請求的過濾、日誌的打印、流量的統計等一系列功能。但是這麼多的過濾器,並不是在每一種場景下都是要全部加載進來的。我們需要能夠根據請求的參數,自動激活要加載的過濾器。這樣的功能,就是通過自激活擴展來實現。

自激活擴展使用@Activate來標記。

@Activate的使用

@Activate註解可以使用在類、接口、枚舉類和方法上。其註解源碼如下

Documented
Retention(RetentionPolicy.RUNTIME)
Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {
    String[] group() default {};

    String[] value() default {};

    String[] before() default {};

    String[] after() default {};

    int order() default 0;
}

通過源碼,可以清楚的看到@Activate註解支持傳入的參數類型。

  • group和value代表激活的條件,group代表URL中的分組,value代表URL中的key。如果匹配條件,則會激活擴展。
  • before、after和order指定了擴展使用的順序。before和after可以指定在特定的擴展前/後加載使用;order爲整型,表示的是直接的排序信息。

具體的@Activate使用示例,請直接移步官網,這裏不再贅述

自激活擴展如何實現自激活

在真正瞭解自激活擴展的加載過程前,我們先來自己想一想,如何實現一個擴展類的自激活?或者說獲取一個自激活擴展類,我們要做哪些事情?

根據上述的描述,你應該知道,自激活擴展加載最終結果會返回一個擴展類集合。
對於每個擴展類的加載,使用上一節我們瞭解的知識就可以加載。
所以,對於自激活擴展來說,我們額外要做的,就是對於擴展類集合的“激活”。

對於“激活”而言,我們主要需要:

  1. 能夠根據條件,過濾篩選出需要的擴展類集合
  2. 能夠根據條件,對篩選後的擴展類集合進行排序

這樣“激活”過的擴展類集合,應當就是符合我們要求的自激活擴展了。
下面我們來看一下,Dubbo中是如何做的吧。

自激活擴展的加載原理

下面我們從源碼角度來看一下自激活擴展是如何加載的。

首先,咱們還是從Dubbo中實際調用自激活擴展的一個實例說起(ProtocolFilterWrapper類)

List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);

getExtensionLoader方法已經在上一篇《一個普通的擴展類加載》中講解過了,這裏就不再贅述了。主要看一下getActivateExtension是如何處理的。

public List<T> getActivateExtension(URL url, String key, String group) {
	String value = url.getParameter(key);
	return getActivateExtension(url, value == null || value.length() == 0 ? null : Constants.COMMA_SPLIT_PATTERN.split(value), group);
}

這裏主要也是進行一個方法參數的透傳,具體涉及到了Dubbo中的URL參數key的解析,這裏先不用太關注,具體還是進入實際調用方法中來看。

public List<T> getActivateExtension(URL url, String[] values, String group) {
List<T> exts = new ArrayList<T>();
    List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
    // $-- 如果擴展點名稱列表中不含-default選項,則加載全部默認的自激活擴展點,篩選加入自激活擴展集合中
    if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
        // $-- 生成擴展類
        getExtensionClasses();
        // $-- 遍歷緩存中的默認自激活擴展
        for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
            String name = entry.getKey();
            Activate activate = entry.getValue();
            // $-- 如果緩存中的自激活擴展類匹配條件,則加入自激活擴展集合中
            // $-- 1. group匹配
            if (isMatchGroup(group, activate.group())) {
                T ext = getExtension(name);
                // $-- 2. 該默認擴展點不在要獲取的擴展點集合中(下面會專門處理要獲取的擴展點) && 要獲取的擴展點集合不存在該默認擴展點的自反 && 此擴展點的value與url匹配
                if (!names.contains(name)
                        && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
                        && isActive(activate, url)) {
                    exts.add(ext);
                }
            }
        }
        // $-- 按照@Activate配置的before、after、order等參數進行排序
        Collections.sort(exts, ActivateComparator.COMPARATOR);
    }
    // $-- 根據要獲取的擴展點名稱列表,獲取擴展點,進行過濾篩選,加入自激活擴展集合中
    List<T> usrs = new ArrayList<T>();
    for (int i = 0; i < names.size(); i++) {
        String name = names.get(i);
        // $-- 如果擴展點名稱不是以-開頭 && 擴展點集合中沒有該擴展點的“自反”(如:既含有name,又含有-name,這樣自相矛盾的不處理)
        if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX)
                && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) {
            if (Constants.DEFAULT_KEY.equals(name)) {
                // $-- 如果擴展點名稱爲default,則將臨時自激活擴展集合轉移到自激活擴展集合首位(用於排序,這樣default就排到後面了)
                if (!usrs.isEmpty()) {
                    exts.addAll(0, usrs);
                    usrs.clear();
                }
            } else {
                // $-- 加載擴展點,並放入臨時自激活擴展集合中
                T ext = getExtension(name);
                usrs.add(ext);
            }
        }
    }
    if (!usrs.isEmpty()) {
        exts.addAll(usrs);
    }
    return exts;
}

此處的getActivateExtension方法是自激活擴展的主體邏輯,主要的處理邏輯是對自激活擴展點的獲取、篩選和排序。

獲取擴展點的邏輯,我們已經在上一篇文章中介紹了,這裏不再贅述。另外說明一下,這裏的自激活擴展點緩存cachedActivates是在文件讀取加載擴展類時,通過loadClass方法“順便”緩存下來的。

對於自激活擴展的篩選和排序,這裏分爲了兩種場景。一種是系統默認的擴展,一種是通過URL參數傳入的用戶自定義擴展。

  1. 對於系統默認的擴展。
    • 篩選:如果參數中沒有-default選項,則加載默認的@Activate擴展。這裏得到的所有默認@Activate擴展點,均需要根據傳入參數的group、url配置的key等進行匹配篩選。對此,註釋中已經有很好的說明了。
    • 排序:默認@Activate擴展點的排序,是按照@Activate配置的before、after、order等參數進行排序,排序的邏輯在ActivateComparator中,這裏不贅述
  2. URL指定的擴展
    • 篩選:由於擴展點名稱是作爲參數傳遞進來的,因此需要進行擴展是否加載的邏輯判斷(如:“-”開頭的擴展不被加載,如“-filter1”代表不加載默認filter1擴展)
    • 排序:對於URL參數傳入的擴展點,用戶可以通過修改URL中參數中擴展點順序來調整擴展點的激活順序。

    舉個例子,如果URL爲 dubbo://localhost/test?ext=filter1,default,則擴展點ext的激活順序爲先filter1再default(URL參數中的“default”代表默認的自激活擴展)

到這裏處理完成,就能獲取指定的條件下(URL)可以使用的自激活擴展列表了。不知道與你想象的實現有多少出入呢?😄

自適應擴展

下面,讓我們來了解Dubbo中使用非常廣泛的另一種擴展——自適應擴展。

什麼是自適應擴展

一個擴展接口會有多種實現類,具體使用哪個實現類並不是寫死在配置或代碼中的,而是在運行時,通過URL參數來動態確定的。這種擴展就叫做自適應擴展。

自適應擴展使用@Adaptive來標註。

@Adaptive的使用

Adaptive可以用在類和接口方法上。

  • 標記在類上,則代表該類爲默認的擴展點實現。如:AdaptiveCompiler爲Compiler的默認實現
  • 標記在方法上,則代表擴展點需要根據參數動態生成。如Dispatcher接口:
SPI(AllDispatcher.NAME)
public interface Dispatcher {
    @Adaptive({Constants.DISPATCHER_KEY, "dispather", "channel.handler"})
    ChannelHandler dispatch(ChannelHandler handler, URL url);
}

Dispatcher接口的dispatch方法上標註了@Adaptive註解,代表這個方法調用時,會自動尋找系統中匹配的擴展類。匹配時會根據@Adaptive註解的value值進行選擇,順序依次爲 常量 Constants.DISPATCHER_KEY,dispatcher,channel.handler。按照順序,只要匹配上某一個擴展類就直接返回,調用該擴展類的dispatch方法。

這裏簡化了一下匹配的規則,方便理解。具體的規則,會在下方源碼分析的時候介紹。

自適應擴展如何實現自適應

在正式的源碼解讀之前,我們不妨就自適應擴展的這種表現,思考一下,如何實現一個自適應擴展?
自適應擴展依然是一個擴展,所以我們理論上依然可以使用Dubbo SPI的普通擴展來加載各種各樣的擴展。但是如何實現自適應呢?
自適應(這裏主要討論@Adaptive註解標註在方法上的自適應)要求能夠根據不同的擴展點,實現不同的匹配規則,來進行擴展點的選擇,最後再來調用選中擴展類的該方法。

拿上述Dispatcher爲例,爲了實現擴展點選擇,應該有這樣一段基本代碼

ChannelHandler dispatch(ChannelHandler handler, URL url) {
	String extName = url.getParameter("dispatcher", url.getParameter("dispather", url.getParameter("channel.handler", "all")));;
	Dispatcher dispatcher = ExtensionLoader.getExtensionLoader(Dispatcher.class).getExtension(extName);
	return dispatcher.dispatch(handler, url);
}

很顯然,這樣的代碼是特殊的,無法公用的。Dubbo顯然不會爲每一個擴展點寫這樣一段代碼。那麼如何生成這樣的代碼呢?
自然而然的,我們想到可以使用javassist、cglib、jdk動態代理來生成這樣的“自適應代碼”,從而實現公用、方便擴展。事實上,Dubbo確實也這麼做了,接下來讓我們從源碼角度來看一下Dubbo自適應擴展是如何玩起來的。

自適應擴展的加載原理

按照慣例,我們依然從一個簡單的調用開始我們的源碼分析。

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

這行代碼先是獲取Protocol的擴展加載器,然後使用加載器去獲得自適應的擴展。前半段不再贅述,讓我們將目光轉到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);
                    }
                }
            }
        } else {
            throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
        }
    }

    return (T) instance;
}

這段代碼主要還然是緩存的老套路,緩存的是自適應擴展類的實例。真正的創建自適應擴展要看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方法進行setter擴展注入。
injectExtension方法我們已經在上一篇解讀過了,這裏不再贅述。來看一下getAdaptiveExtensionClass方法。

private Class<?> getAdaptiveExtensionClass() {
	// $-- 加載所有擴展類
    getExtensionClasses();
    // $-- 如果緩存有默認的自適應擴展類,則直接返回(這裏主要是@Adaptive標記在擴展類上的場景,該擴展類爲該擴展點的默認擴展類)
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    // $-- 生成自適應擴展類
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

通過getExtensionClasses方法加載所有擴展類也已經在上一篇瞭解過了,這裏不再贅述。

通過cachedAdaptiveClass緩存的判斷,體現了@Adaptive標記在擴展類上即爲默認擴展類的這一設計。cachedAdaptiveClass是在從文件讀取加載擴展類,loadClass時賦值的。

下面還是像“套娃”一樣,進入createAdaptiveExtensionClass方法

 private Class<?> createAdaptiveExtensionClass() {
    // $-- 生成自適應擴展類的代碼
    String code = createAdaptiveExtensionClassCode();
    ClassLoader classLoader = findClassLoader();
    // $-- 獲取Compiler的擴展點實現類,編譯生成的代碼爲Class
    com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    return compiler.compile(code, classLoader);
}

從這段代碼裏我們已經可以大致驗證自適應的猜想了。
首先通過createAdaptiveExtensionClassCode方法,生成該擴展點的“自適應代碼”。
然後獲取類加載器和編譯器。利用編譯器編譯得到最終的自適應擴展類。

需要注意的是這裏的編譯器Compiler依然是一個自適應擴展類。那是否就構成了一個死循環了呢?
當然不可能,爲了不打擾主體流程,具體原因放在最後討論。這裏先知道能否獲取到Compiler,並且默認爲javassist擴展。

讓我們繼續看一下代碼是如何生成的吧!且看createAdaptiveExtensionClassCode方法

private String createAdaptiveExtensionClassCode() {
   StringBuilder codeBuilder = new StringBuilder();
    Method[] methods = type.getMethods();
    // $-- 校驗是否有@Adaptive註解(至少應當有一個方法有Adaptive註解)
    boolean hasAdaptiveAnnotation = false;
    for (Method m : methods) {
        if (m.isAnnotationPresent(Adaptive.class)) {
            hasAdaptiveAnnotation = true;
            break;
        }
    }
    // no need to generate adaptive class since there's no adaptive method found.
    if (!hasAdaptiveAnnotation)
        throw new IllegalStateException("No adaptive method on extension " + type.getName() + ", refuse to create the adaptive class!");

    // $-- 生成包、引用等代碼
    codeBuilder.append("package ").append(type.getPackage().getName()).append(";");
    codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";");
    codeBuilder.append("\npublic class ").append(type.getSimpleName()).append("$Adaptive").append(" implements ").append(type.getCanonicalName()).append(" {");

    // $-- 遍歷生成方法代碼
    for (Method method : methods) {
        Class<?> rt = method.getReturnType();
        Class<?>[] pts = method.getParameterTypes();
        Class<?>[] ets = method.getExceptionTypes();

        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        StringBuilder code = new StringBuilder(512);
        if (adaptiveAnnotation == null) {
            // $-- 如果該方法沒有@Adaptive註解,則生成拋異常代碼
            code.append("throw new UnsupportedOperationException(\"method ")
                    .append(method.toString()).append(" of interface ")
                    .append(type.getName()).append(" is not adaptive method!\");");
        } else {
            // $-- 如果該方法有@Adaptive註解,即爲自適應方法
            // $-- 判斷參數中是否有URL
            int urlTypeIndex = -1;
            for (int i = 0; i < pts.length; ++i) {
                if (pts[i].equals(URL.class)) {
                    urlTypeIndex = i;
                    break;
                }
            }
            // found parameter in URL type
            if (urlTypeIndex != -1) {
                // $-- 生成校驗URL參數不能爲空代碼
                // Null Point check
                String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");",
                        urlTypeIndex);
                code.append(s);

                s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex);
                code.append(s);
            }
            // did not find parameter in URL type
            else {
                // $-- 方法參數中沒有直接URL類型參數
                String attribMethod = null;

                // find URL getter method
                LBL_PTS:
                for (int i = 0; i < pts.length; ++i) {
                    // $-- 嘗試遍歷方法參數列表中每一個類中是否有滿足要求的類(get開頭,返回URL,無參數,公共靜態)
                    Method[] ms = pts[i].getMethods();
                    for (Method m : ms) {
                        String name = m.getName();
                        if ((name.startsWith("get") || name.length() > 3)
                                && Modifier.isPublic(m.getModifiers())
                                && !Modifier.isStatic(m.getModifiers())
                                && m.getParameterTypes().length == 0
                                && m.getReturnType() == URL.class) {
                            urlTypeIndex = i;
                            attribMethod = name;
                            break LBL_PTS;
                        }
                    }
                }
                if (attribMethod == null) {
                    throw new IllegalStateException("fail to create adaptive class for interface " + type.getName()
                            + ": not found url parameter or url attribute in parameters of method " + method.getName());
                }

                // $-- 生成空指針校驗代碼
                // Null point check
                String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");",
                        urlTypeIndex, pts[urlTypeIndex].getName());
                code.append(s);
                s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");",
                        urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod);
                code.append(s);

                s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod);
                code.append(s);
            }

            String[] value = adaptiveAnnotation.value();
            // value is not set, use the value generated from class name as the key
            if (value.length == 0) {
                // $-- "駝峯規則"匹配,如 YyyInvokerWrapper ==> yyy.invoker.wrapper
                char[] charArray = type.getSimpleName().toCharArray();
                StringBuilder sb = new StringBuilder(128);
                for (int i = 0; i < charArray.length; i++) {
                    if (Character.isUpperCase(charArray[i])) {
                        if (i != 0) {
                            sb.append(".");
                        }
                        sb.append(Character.toLowerCase(charArray[i]));
                    } else {
                        sb.append(charArray[i]);
                    }
                }
                value = new String[]{sb.toString()};
            }

            // $-- 生成Invocation類空校驗代碼
            boolean hasInvocation = false;
            for (int i = 0; i < pts.length; ++i) {
                if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) {
                    // Null Point check
                    String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i);
                    code.append(s);
                    s = String.format("\nString methodName = arg%d.getMethodName();", i);
                    code.append(s);
                    hasInvocation = true;
                    break;
                }
            }

            // $-- 生成獲取擴展點名稱的代碼
            String defaultExtName = cachedDefaultName;
            String getNameCode = null;
            for (int i = value.length - 1; i >= 0; --i) {
                if (i == value.length - 1) {
                    if (null != defaultExtName) {
                        if (!"protocol".equals(value[i]))
                            if (hasInvocation)
                                getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                            else
                                getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
                        else
                            getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);
                    } else {
                        if (!"protocol".equals(value[i]))
                            if (hasInvocation)
                                getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                            else
                                getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
                        else
                            getNameCode = "url.getProtocol()";
                    }
                } else {
                    if (!"protocol".equals(value[i]))
                        if (hasInvocation)
                            getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                        else
                            getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
                    else
                        getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
                }
            }
            code.append("\nString extName = ").append(getNameCode).append(";");
            // check extName == null?
            String s = String.format("\nif(extName == null) " +
                            "throw new IllegalStateException(\"Fail to get extension(%s) name from url(\" + url.toString() + \") use keys(%s)\");",
                    type.getName(), Arrays.toString(value));
            code.append(s);

            // $-- 生成獲取具體擴展實現類代碼
            s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);",
                    type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
            code.append(s);

            // $-- 生成調用方法代碼
            // return statement
            if (!rt.equals(void.class)) {
                code.append("\nreturn ");
            }

            s = String.format("extension.%s(", method.getName());
            code.append(s);
            for (int i = 0; i < pts.length; i++) {
                if (i != 0)
                    code.append(", ");
                code.append("arg").append(i);
            }
            code.append(");");
        }

        // $-- 補齊生成方法的頭尾代碼
        codeBuilder.append("\npublic ").append(rt.getCanonicalName()).append(" ").append(method.getName()).append("(");
        for (int i = 0; i < pts.length; i++) {
            if (i > 0) {
                codeBuilder.append(", ");
            }
            codeBuilder.append(pts[i].getCanonicalName());
            codeBuilder.append(" ");
            codeBuilder.append("arg").append(i);
        }
        codeBuilder.append(")");
        if (ets.length > 0) {
            codeBuilder.append(" throws ");
            for (int i = 0; i < ets.length; i++) {
                if (i > 0) {
                    codeBuilder.append(", ");
                }
                codeBuilder.append(ets[i].getCanonicalName());
            }
        }
        codeBuilder.append(" {");
        codeBuilder.append(code.toString());
        codeBuilder.append("\n}");
    }
    codeBuilder.append("\n}");
    if (logger.isDebugEnabled()) {
        logger.debug(codeBuilder.toString());
    }
    return codeBuilder.toString();
}

這裏不得不吐槽一下,這個方法實在是太長了。。。😂
簡單來說,爲了拼接出自適應的代碼,做了這樣幾件事情:

  1. 校驗該擴展點的方法是否有@Adaptive註解。至少有一個方法有@Adaptive註解,這樣纔有生成自適應擴展的必要
  2. 通過拼接字符串的方式,生成包、引用等代碼
  3. 遍歷該擴展點所有的方法,生成方法代碼
    3.1 如果該方法上沒有@Adaptive註解,則不允許該方法自適應調用,生成拋異常代碼
    3.2 如果方法上有@Adaptive註解,則需要生成自適應方法
  4. 類所有代碼的拼接

其中,生成自適應方法主要邏輯如下:

  1. 生成獲取默認擴展類的代碼
  2. 生成判斷擴展類爲空的代碼
  3. 生成擴展類方法調用的代碼

整個邏輯還是拼接代碼的套路,需要注意的就是擴展類的獲取邏輯。自適應代碼生成邏輯中大量的代碼都是在處理這個邏輯。
擴展類依然還是通過ExtensionLoader.getExtension(extName)來生成的,唯一要注意的是這裏擴展點名稱extName的取值邏輯。
擴展點名稱extName是從URL中的鍵值對獲取的。那麼URL中的鍵key又是如何獲取的呢?這裏簡單總結如下:

  1. 先取@Adaptive註解中的key
  2. 如果@Adaptive註解未配置key,則會根據當前擴展點的名稱,生成key(“駝峯規則”,如YyyInvokerWrapper ==> yyy.invoker.wrapper)
  3. 如果依然沒有找到,則取擴展點@SPI註解中的key

走到這裏,我們終於把自適應擴展關鍵的代碼生成邏輯講解完成了。剩下的就是使用編譯器來進行編譯了,編譯過程就不贅述了。

這裏我們遺留了一個問題,Compiler也是一個自適應擴展,那麼走到這裏會不會形成一個死循環呢?讓我們繼續看一下Compiler自適應擴展是如何加載的

Compiler自適應擴展類是如何加載的

Compiler自適應擴展會不會形成死循環呢?
答案當然是NO!因爲Compiler擴展點的自適應擴展類並不是通過生成代碼而獲取的。

不知你是否還記得,如果@Adaptive標記在類上時,表示這個類是該擴展點的默認實現類。而Compiler就是利用這一點進行加載的。

在AdaptiveCompiler類上有@Adaptive標識,代表AdaptiveCompiler爲Compiler默認的擴展實現

@Adaptive
public class AdaptiveCompiler implements Compiler {

    private static volatile String DEFAULT_COMPILER;

    public static void setDefaultCompiler(String compiler) {
        DEFAULT_COMPILER = compiler;
    }

    @Override
    public Class<?> compile(String code, ClassLoader classLoader) {
        Compiler compiler;
        ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
        String name = DEFAULT_COMPILER; // copy reference
        if (name != null && name.length() > 0) {
            compiler = loader.getExtension(name);
        } else {
            compiler = loader.getDefaultExtension();
        }
        return compiler.compile(code, classLoader);
    }
}

在我們的應用程序中未指定DEFAULT_COMPILER的情況下,默認會使用默認擴展(javassist)作爲compiler。看loader.getDefaultExtension方法

public T getDefaultExtension() {
    getExtensionClasses();
    if (null == cachedDefaultName || cachedDefaultName.length() == 0
            || "true".equals(cachedDefaultName)) {
        return null;
    }
    return getExtension(cachedDefaultName);
}

getDefaultExtension方法會取cachedDefaultName作爲默認擴展類名。那麼這個cachedDefaultName又是從哪裏獲取的呢?

這就要追溯到文件中讀取加載擴展類的過程了。在真正查找文件之前,會有一個SPI註解的判斷,具體實現在loadExtensionClasses方法中

private Map<String, Class<?>> loadExtensionClasses() {
 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);
            // $-- @SPI註解只允許指定一個默認值
            if (names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
                        + ": " + Arrays.toString(names));
            }
            // $-- @SPI指定了一個默認值,緩存起來
            if (names.length == 1) cachedDefaultName = names[0];
        }
    }

    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    // $-- 加載擴展類資源目錄
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    ...
}

如果擴展點@SPI註解的value有且只有一個值,那麼這個值就是默認擴展類。我們再看Compiler接口

@SPI("javassist")
public interface Compiler {
    Class<?> compile(String code, ClassLoader classLoader);
}

你看,這樣就指定javassist爲默認擴展類了。

在調用getAdaptiveExtension獲取自適應擴展時,判斷到如果有默認的自適應擴展,就會直接返回該擴展類。

private Class<?> getAdaptiveExtensionClass() {
    // $-- 加載所有擴展類
    getExtensionClasses();
    // $-- 如果緩存有默認的自適應擴展類,則直接返回(這裏主要是@Adaptive標記在擴展類上的場景,該擴展類爲該擴展點的默認擴展類)
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    // $-- 生成自適應擴展類
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

總結

到此爲止,Dubbo SPI機制主要內容就已經介紹完成了。
Dubbo SPI機制是理解後續服務暴露、引用和調用過程的基石,也是整個Dubbo能夠博採衆長,吸納衆多開源解決方案的一個重要原因。
整個流程可能有點繞,但就我而言,通過實際Debug對於理解這套機制非常有用。

不知你對Dubbo SPI瞭解有多少了呢?😁

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