Dubbo 重點,SPI 的自適應擴展原理

本文從爲什麼需要自適應擴展的提問引出自己如何實現以及推理 Dubbo 如何實現,這些鋪墊可以幫助讀者更好的理解後文對 Dubbo 自適應擴展源碼的解讀。

很多人在學習 SPI 的時候將@SPI@Adaptive註解混在一起學習,最後學得暈暈乎乎看完之後似懂非懂,如果你也有這種困擾,請繼續閱讀。

並不是說不該將這兩個內容一起學習,而是要有個先後順序再加上自己的推理。是先有 SPI 機制,然後纔有的自適應擴展,自適應擴展是基於 SPI 機制的高級特性。如果你還不懂 Dubbo SPI 原理,請移步這篇文章。

Dubbo SPI 機制核心原理,你掌握了嗎?|原創

爲什麼需要自適應擴展點

在 Dubbo 中,很多拓展都是通過 SPI 機制進行加載的,比如 Protocol、Cluster、LoadBalance 等。有時,有些拓展並不想在框架啓動階段被加載,而是希望在拓展方法被調用時,根據運行時參數進行加載。這聽起來有些矛盾。拓展未被加載,那麼拓展方法就無法被調用(靜態方法除外)。拓展方法未被調用,拓展就無法被加載。對於這個矛盾的問題,Dubbo 通過自適應拓展機制很好的解決了。

對於這個問題,以之前 demo 爲例進行我們進行推演:

/**
 * @author 後端開發技術
 */
public interface MySPI {
    void say();
}
public class HelloMySPI implements MySPI{
    @Override
    public void say() {
        System.out.println("HelloMySPI say:hello");
    }
}
public class GoodbyeMySPI implements MySPI {
    @Override
    public void say() {
        System.out.println("GoodbyeMySPI say:Goodbye");
    }
}

現在要增加一個接口 Person,他可以和人打招呼。他有一個實現類是Man,他可以動態的跟人說 hello 或者 goodbye。

public interface Person {

    void greeting();
}

public class Man implements Person{
    private MySPI adaptive;

    public void setAdaptive(MySPI adaptive) {
        this.adaptive = adaptive;
    }

    @Override
    public void greeting(URL url) {
        adaptive.say(url);
    }

}

但是adaptive成員要麼是HelloMySPI的實例化對象,要麼是GoodbyeMySPI的實例化對象,怎麼實現動態的去根據需要獲取呢?解決這個問題就可以增加一個代理,作爲自適應類。所以增加自適應擴展實現如下:

public class AdaptiveMySPI implements MySPI {

    @Override
    public void say() {
        // 1.通過某種方式獲得拓展實現的名稱
        String mySpiName;

        // 2.通過 SPI 加載具體的 mySpi
        MySPI myspi = ExtensionLoader.getExtensionLoader(MySPI.class).getExtension(mySpiName);

        // 3.調用目標方法
        myspi.say();
    }
}

將代理類AdaptiveMySPI作爲Man的成員對象,這樣就可以實現按需調用了。按需加載如何實現呢?之前我們在getExtension()方法中提到過,只要在根據名字查找的時候,纔會按照需要懶加載,所以這個問題天然被 Dubbo SPI 解決了。那麼剩下的關鍵就是如何按需調用,也就是如何獲得名字。

  1. 可以在當前線程的上下文中獲得,比如通過 ThreadLocal 保存。

  2. 可以通過接口參數傳遞,但是這樣就需要實現自適應擴展的接口按照約定去定義參數,否則就無法拿到名字,這樣對於被代理的接口是有一定限制的。

Dubbo 用的是第二種方式,也就是他總有辦法從參數中動態拿到擴展類名。

模擬原理 demo

再具體一些 Dubbo 是怎麼實現的呢?

自適應拓展機制的實現邏輯比較複雜,首先 Dubbo 會爲拓展接口生成具有代理功能的代碼。然後通過 javassist 或 jdk 編譯這段代碼,得到 Class 類。最後再通過反射創建代理類,整個過程比較複雜。爲了讓大家對自適應拓展有一個感性的認識,按照之前的知識,下面我們繼續對之前 demo 爲例進行改造:

@SPI
public interface MySPI {
    void say(URL url);
}
public class HelloMySPI implements MySPI{
    @Override
    public void say(URL url) {
        System.out.println("HelloMySPI say:hello");
    }
}
public class GoodbyeMySPI implements MySPI {
    @Override
    public void say(URL url) {
        System.out.println("GoodbyeMySPI say:Goodbye");
    }
}
public class AdaptiveMySPI implements MySPI {

    @Override
    public void say(URL url) {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }

        // 1.從 URL 中獲取 mySpi 名稱
        String mySpiName = url.getParameter("myspi.type");
        if (mySpiName == null) {
            throw new IllegalArgumentException("MySPI == null");
        }

        // 2.通過 SPI 加載具體的 mySpi
        MySPI myspi = ExtensionLoader.getExtensionLoader(MySPI.class).getExtension(mySpiName);

        // 3.調用目標方法
        myspi.say(url);
    }
}
@SPI("man")
public interface Person {

    void greeting(URL url);
}
public class Man implements Person {
    private MySPI adaptive = = ExtensionLoader.getExtensionLoader(MySPI.class).getExtension(“adaptive”);

    public void setAdaptive(MySPI adaptive) {
        this.adaptive = adaptive;
    }

    @Override
    public void greeting(URL url) {
        adaptive.say(url);
    }
}
public static void main(String[] args) {
        ExtensionLoader<Person> extensionLoader = ExtensionLoader.getExtensionLoader(Person.class);
        Person hello = extensionLoader.getExtension("man");
        hello.greeting(URL.valueOf("dubbo://192.168.0.101:100?myspi.type=hello"));
        hello.greeting(URL.valueOf("dubbo://192.168.0.101:100?myspi.type=goodbye"));
    }
//輸出
HelloMySPI say:hello
GoodbyeMySPI say:Goodbye

大家與之前的代碼對比就可以發現區別,MySPI 的方法增加了 URL 參數,因爲 dubbo 中 url 就是作爲一個配置總線貫穿整個調用鏈路的。這樣便可以拿到擴展名,動態調用和加載了。

比如 demo 中的一個 URL

dubbo://192.168.0.101:100?myspi.type=hello

AdaptiveMySPI 動態的人從 url 中拿到了myspi.type=hello,然後根據 name 拿到了擴展實現,以此完成動態調用。

上面的示例展示了自適應拓展類的核心實現 —- 在拓展接口的方法被調用時,通過 SPI 加載具體的拓展實現類,並調用拓展對象的同名方法。所以接下來的關鍵就在於,自適應拓展類是如何生成的,Dubbo 是怎麼做的。

@Adaptive 註解

關於 Dubbo 的自適應擴展,一定避不開這個關鍵註解@Adaptive

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

從上面的代碼中可知,Adaptive 可註解在類或方法上。

Adaptive 註解的地方不同,相應的處理邏輯也是不同的。註解在類上時,處理邏輯比較簡單,本文就不分析了。註解在接口方法上時,處理邏輯較爲複雜,本章將會重點分析此塊邏輯。

你有沒有想過爲什麼可以設計加載在類上?

按照我們上述的推理,實現自適應擴展總需要按照一定的約束和規範去設計方法參數,這樣才能拿到參數去動態的加載擴展類。如果就是沒有按照規範去設計參數呢?那就需要自己去想辦法實現了,無法生成代理類,這就是加載在類上的原因。

源碼解讀

獲得自適應類

在 Dubbo 中如何獲得一個自適應擴展類呢?只需要一行代碼。

MySPI adaptive = ExtensionLoader.getExtensionLoader(MySPI.class).getAdaptiveExtension();
@SPI
public interface MySPI {

    @Adaptive(value = {"myspi.type"})
    void say(URL url);
}

getExtensionLoader()之前我們依舊分析過了,這裏直接從getAdaptiveExtension()開始。

getAdaptiveExtension() 方法是獲取自適應拓展的入口方法,首先會檢查緩存cachedAdaptiveInstance,緩存未命中,則會執行雙重檢查,調用 createAdaptiveExtension() 方法創建自適應拓展。

public T getAdaptiveExtension() {
    // 從緩存中查找自適應擴展類實例,命中直接返回
    Object instance = cachedAdaptiveInstance.get();
   //緩存沒有命中
    if (instance == null) {
            …… 異常處理
        //雙重檢查
        synchronized (cachedAdaptiveInstance) {
            instance = cachedAdaptiveInstance.get();
            if (instance == null) {
                try {
                    // 創建自適應實例
                    instance = createAdaptiveExtension();
                    cachedAdaptiveInstance.set(instance);
                } catch (Throwable t) {
                   …… 異常處理
    return (T) instance;
}

createAdaptiveExtension()會首先查找只適應擴展類,然後通過反射進行實例化,再調用injectExtension對擴展實例中注入依賴。

你可能會問直接返回依賴不就行了?爲什麼還需要注入?

這是因爲任何利用 Dubbo SPI 機制加載的用戶創建類都是有可能有成員依賴於其他拓展類的,用戶實現的自適應擴展類也不例外。而另一種 Dubbo 自己生成的自適應擴展類則不可能出現依賴其他類的情況。

這裏只關注重點方法getAdaptiveExtensionClass()

  1. 首先getExtensionClasses()會獲取該接口所有的拓展類,

  2. 然後會檢查緩存是否爲空,cachedAdaptiveClass緩存着自適應擴展類的類型。

  3. 如果緩存中不存在則調用createAdaptiveExtensionClass開始創建自適應擴展類。

// 用於緩存自適應擴展類的類型
private volatile Class<?> cachedAdaptiveClass = null;
private Class<?> getAdaptiveExtensionClass() {
    // 加載所有的拓展類配置
    getExtensionClasses();
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    // 創建自適應拓展類
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

getExtensionClasses之前講過,你肯定會奇怪爲什麼還需要加載所有擴展類。在這裏有個關鍵邏輯,在調用 loadResource 方法時候會解析 @Adaptive 註解,如果被標註了,就表示這個類是一個自適應擴展類實現,會被設置到緩存cacheAdaptiveClass中。

所以有兩個原因:

1、是自定義自適應擴展類需要 SPI 機制加載

2、是設置緩存

生成自適應擴展類

非自己實現的自適應擴展類,都要走createAdaptiveExtensionClass邏輯。

主要邏輯如下:

  1. 動態生成自適應擴展類代碼

  2. 獲取類加載器和編譯器類(Dubbo 默認使用 javassist 作爲編譯器)

  3. 編譯、加載動態生成的類

private Class<?> createAdaptiveExtensionClass() {
    // 動態生成自適應擴展類代碼
    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
    // 獲取類加載器
    ClassLoader classLoader = findClassLoader();
    // 獲取編譯器類 ⚠️ AdaptiveCompiler 也是自己定義的
    org.apache.dubbo.common.compiler.Compiler compiler =  ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    //編譯、加載、生產Class
    return compiler.compile(code, classLoader);
}

生成自適應擴展類的代碼都在AdaptiveClassCodeGenerator中,generate()方法會生成和返回一個自適應擴展類。之前的版本代碼其實比較複雜,邏輯都寫在了一起,並沒有 generate 方法,進入 Apache 孵化之後對代碼結構進行了調整,結構清晰了許多。

主要邏輯如下:

  1. 檢查接口是否有方法被 @Adaptive 修飾。

  2. 生產 class 頭部的 package 信息。

  3. 生成依賴類的 import 信息。

  4. 生成方法聲明信息。

  5. 遍歷接口方法依次生成實現方法。

  6. 類結束用}收尾,類信息轉換爲字符串返回。

public String generate() {
    // no need to generate adaptive class since there's no adaptive method found.
    // 檢查接口是否有註解了Adaptive的方法,至少需要有一個
    if (!hasAdaptiveMethod()) {
        throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
    }

    StringBuilder code = new StringBuilder();
    // 生成package信息
    code.append(generatePackageInfo());
    // 生成依賴類的import信息
    code.append(generateImports());
    // 生成類的聲明信息 public class 接口名$Adaptive implements 接口名
    code.append(generateClassDeclaration());

    // 遍歷接口方法 按需生產實現類
    Method[] methods = type.getMethods();
    for (Method method : methods) {
        code.append(generateMethod(method));
    }
  // 類結尾
    code.append("}");

    if (logger.isDebugEnabled()) {
        logger.debug(code.toString());
    }
  // 轉換爲字符串返回
    return code.toString();
}

下面依次介紹上述步驟的主要邏輯。

檢查 @Adaptive 註解

遍歷接口方法依次檢查是否被 @Adaptive 標註,至少需要有一個方法被註解,否則拋出異常。

private boolean hasAdaptiveMethod() {
    return Arrays.stream(type.getMethods()).anyMatch(m -> m.isAnnotationPresent(Adaptive.class));
}

生成 package 和 import 信息

按照接口的路徑名生成對應的 package 信息,並且生成導入信息,目前只是 import 了ExtensionLoader這個類。

private static final String CODE_PACKAGE = "package %s;\n";
private String generatePackageInfo() {
    return String.format(CODE_PACKAGE, type.getPackage().getName());
}
private static final String CODE_IMPORTS = "import %s;\n";
private String generateImports() {
    return String.format(CODE_IMPORTS, ExtensionLoader.class.getName());
}
// import org.apache.dubbo.common.extension.ExtensionLoader;

生成類的聲明信息

生成的類名 = 拓展接口名 +$Adaptive,實現的接口就是拓展接口的全限定名。比如 public class MySPI$Adaptive implements org.daley.spi.demo.MySPI

private String generateClassDeclaration() {
    return String.format(CODE_CLASS_DECLARATION, type.getSimpleName(), type.getCanonicalName());
}
private static final String CODE_CLASS_DECLARATION = "public class %s$Adaptive implements %s {\n";

生成方法體

generateMethod方法是自適應拓展類生成代理類的核心邏輯所在。它主要會分別拿到方法返回類型、方法名、生成方法體、生成方法參數、生成方法異常,然後按照方法的模板的佔位符生成代理方法。很明顯,重中之重是生成方法體內容。

private String generateMethod(Method method) {
    // 分別拿到方法返回類型、方法名、方法體、方法參數、方法異常
    String methodReturnType = method.getReturnType().getCanonicalName();
    String methodName = method.getName();
    // 生成方法體
    String methodContent = generateMethodContent(method);
    String methodArgs = generateMethodArguments(method);
    String methodThrows = generateMethodThrows(method);
    // 按照方法模板替換佔位符,生成方法內容
    return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
}

generateMethodContent主要做了如下幾件事:

  1. 檢查是否被 @Adaptive 註解,如果沒有被註解則生產一段拋出異常的代碼。如果被註解,則繼續後面邏輯。

  2. 找到 URL 類型參數的 index,並且生成檢查 URL 參數是否爲空的邏輯。

  3. 如果沒有 URL 參數,則檢查是否方法參數有 public 類型無參 get 方法可以直接拿到 URL。

  4. 拿到 @Adaptive 註解配置的 value,如果沒有配置就用接口名默認。

  5. 檢查是否有 Invocation 類型參數。

  6. 根據不同的情況拿到拓展名。

  7. 根據擴展名從getExtension中拿到真正的擴展類。

  8. 執行擴展類目標方法,按需返回結果。

這些步驟邏輯都不算複雜,需要格外注意的是第 6 點,這裏詳細再說明下。

private String generateMethodContent(Method method) {
    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    // 檢查是否被@Adaptive註解
    if (adaptiveAnnotation == null) {
        // 不是自適應方法生成的代碼是一段拋出異常的代碼。
        return generateUnsupported(method);
    } else {
        // 找到 URL.class 類型參數位置
        int urlTypeIndex = getUrlTypeIndex(method);

        // found parameter in URL type
        if (urlTypeIndex != -1) {
            // Null Point check
            // 找到 URL 生成代碼邏輯
            code.append(generateUrlNullCheck(urlTypeIndex));
        } else {
            // did not find parameter in URL type
            // 未找到 URL 生成代碼邏輯
            // 再找找是否有方法參數有get方法可以返回URL.class的,並且還是不需要入參的public方法
            code.append(generateUrlAssignmentIndirectly(method));
        }
        // 拿到 Adaptive註解的value
        String[] value = getMethodAdaptiveValue(adaptiveAnnotation);

        // 有 Invocation 類型參數
        boolean hasInvocation = hasInvocationArgument(method);

        // 檢查參數不爲null
        code.append(generateInvocationArgumentNullCheck(method));

        code.append(generateExtNameAssignment(value, hasInvocation));
        // check extName == null?
        code.append(generateExtNameNullCheck(value));
        // getExtension 根據name拿到擴展類
        code.append(generateExtensionAssignment());

        // return statement 生成方法調用語句並在必要時返回
        code.append(generateReturnAndInvocation(method));
    }

    return code.toString();
}

generateExtNameAssignment中會有如下幾種不同的情況:是否最後一個參數,是否有 Invocation 類型參數、是否配置了名字爲protocol的註解 value。

將上面的三種條件組合,生成對應不同的代碼,核心其實都是如何正確的從 URL 參數中拿到動態擴展名,具體已做註釋。

private String generateExtNameAssignment(String[] value, boolean hasInvocation) {
    // TODO: refactor it
    String getNameCode = null;
    // 從最後一個開始遍歷
    for (int i = value.length - 1; i >= 0; --i) {
        if (i == value.length - 1) {
            // 如果是最後一個參數,設置了默認拓展名
            if (null != defaultExtName) {
                // 配置的value不等於"protocol"
                if (!"protocol".equals(value[i])) {
                    if (hasInvocation) {
                        // 有invocation 根據配置名字從url獲取getMethodParameter
                        getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                    } else {
                        // 沒有invocation,getParameter獲取參數
                        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);
            }
        }
    }

    return String.format(CODE_EXT_NAME_ASSIGNMENT, getNameCode);
}

對於上述條件,可以生成如下幾種情況的代碼。

String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
String extName = url.getMethodParameter(methodName, "loadbalance""random");
String extName = url.getParameter("client", url.getParameter("transporter""netty"));

還有一個疑問點是如果有多個參數怎麼辦?按照代碼邏輯,最終的表現效果就是如果有多個參數,且非 Invocation,會生成多層嵌套代碼,並且以最外層也就是最左邊的參數爲準,右邊的參數作爲默認值。

舉個例子:

@Adaptive(value = {"protocol","param2","myspi.type"})
void say(URL url);

生成代碼如下:

url.getProtocol() == null ? (url.getParameter("param2", url.getParameter("myspi.type"))) : url.getProtocol()

當上述一切代碼執行完成後,就生成了最終的代理類,並且經過編譯和加載最終完成實例化,可以被程序所調用,實現動態按需調用。

最終生成的代理類如下:

package org.daley.spi.demo;
import org.apache.dubbo.common.extension.ExtensionLoader;

public class MySPI$Adaptive implements org.daley.spi.demo.MySPI {
 public void say(org.apache.dubbo.common.URL arg0)  {
 if (arg0 == null) throw new IllegalArgumentException("url == null");
 org.apache.dubbo.common.URL url = arg0;
 String extName = url.getProtocol() == null ?(url.getParameter("param2",url.getParameter("myspi.type"))) : url.getProtocol();
 if(extName == null) throw new IllegalStateException("Failed to get extension  (org.daley.spi.demo.MySPI) name from url (" + url.toString() + ") use keys([protocol, param2, myspi.type])");
 org.daley.spi.demo.MySPI extension = (org.daley.spi.demo.MySPI)ExtensionLoader.getExtensionLoader(org.daley.spi.demo.MySPI.class).getExtension(extName);
 extension.say(arg0);
 }
}

到這裏,Dubbo 自適應擴展的原理就講解結束了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/AIMsttV0sk--bG22k69zlA