Dubbo 源碼分析 - 自適應拓展原理

1.原理

我在上一篇文章中分析了 Dubbo 的 SPI 機制,Dubbo SPI 是 Dubbo 框架的核心。Dubbo 中的不少拓展都是經過 SPI 機制進行加載的,好比 Protocol、Cluster、LoadBalance 等。有時,有些拓展並不是想在框架啓動階段被加載,而是但願在拓展方法被調用時,根據運行時參數進行加載。這聽起來有些矛盾。拓展未被加載,那麼拓展方法就沒法被調用(靜態方法除外)。拓展方法未被調用,就沒法進行加載,這彷佛是個死結。不過好在也有相應的解決辦法,經過代理模式就能夠解決這個問題,這裏咱們將具備代理功能的拓展稱之爲自適應拓展。Dubbo 並未直接經過代理模式實現自適應拓展,而是代理代理模式基礎上,封裝了一個更炫的實現方式。Dubbo 首先會爲拓展接口生成具備代理功能的代碼,而後經過 javassist 或 jdk 編譯這段代碼,獲得 Class 類,最後在經過反射建立代理類。整個過程比較複雜、炫麗。如此複雜的過程最終的目的是爲拓展生成代理對象,但實際上每一個代理對象的代理邏輯基本一致,均是從 URL 中獲取欲加載實現類的名稱。所以,咱們徹底能夠把代理邏輯抽出來,並經過動態代理的方式實現自適應拓展。這樣作的好處顯而易見,方便維護,也方便源碼學習者學習和調試代碼。本文將在隨後實現一個動態代理版的自適應拓展,有興趣的同窗能夠繼續往下讀。java

接下來,咱們經過一個示例演示自適應拓展類。這個示例取自 Dubbo 官方文檔,我這裏進行了必定的拓展。這是一個與汽車相關的例子,咱們有一個車輪製造廠接口 WheelMaker:數組

public interface WheelMaker {
    Wheel makeWheel(URL url);
}

WheelMaker 接口的 Adaptive 實現類以下:緩存

public class AdaptiveWheelMaker implements WheelMaker {
    public Wheel makeWheel(URL url) {
        if (url == null) {
            throw new IllegalArgumentException("url == null");
        }
        
        // 1.從 URL 中獲取 WheelMaker 名稱
        String wheelMakerName = url.getParameter("Wheel.maker");
        if (name == null) {
            throw new IllegalArgumentException("wheelMakerName == null");
        }
        
        // 2.經過 SPI 加載具體的 WheelMaker
        WheelMaker wheelMaker = ExtensionLoader
            .getExtensionLoader(WheelMaker.class).getExtension(wheelMakerName);
        
        // 3.調用目標方法
        return wheelMaker.makeWheel(URL url);
    }
}

AdaptiveWheelMaker 是一個代理類,它主要作了三件事情:app

  1. 從 URL 中獲取 WheelMaker 名稱
  2. 經過 SPI 加載具體的 WheelMaker
  3. 調用目標方法

接下來,咱們來看看汽車製造廠 CarMaker 接口與其實現類。框架

public interface CarMaker {
    Car makeCar(URL url);
}

public class RaceCarMaker implements CarMaker {
    WheelMaker wheelMaker;
 
    // 經過 setter 注入 AdaptiveWheelMaker
    public setWheelMaker(WheelMaker wheelMaker) {
        this.wheelMaker = wheelMaker;
    }
 
    public Car makeCar(URL url) {
        Wheel wheel = wheelMaker.makeWheel(url);
        return new RaceCar(wheel, ...);
    }
}

RaceCarMaker 持有一個 WheelMaker 類型從成員變量,在程序啓動時,咱們能夠將 AdaptiveWheelMaker 經過 setter 方法注入到 RaceCarMaker 中。在運行時,假設有這樣一個 URL 類型的參數:dom

dubbo://192.168.0.101:20880/XxxService?wheel.maker=MichelinWheelMaker

RaceCarMaker 的 makeCar 方法將上面的 url 做爲參數傳給 AdaptiveWheelMaker 的 makeWheel 方法,makeWheel 方法從 url 中提取 wheel.maker 參數,獲得 MichelinWheelMaker。以後再經過 SPI 加載名爲 MichelinWheelMaker 的實現類,獲得具體的 WheelMaker 實例。jvm

上面這個示例展現了自適應拓展類的核心實現 -- 在組件方法被調用時,經過代理的方式加載指定的實現類,並調用被代理的方法。ide

通過以上說明,你們應該搞懂了自適應拓展的原理。接下來,咱們深刻到源碼中,探索自適應拓展生成的過程。源碼分析

2.源碼分析

在對自適應拓展生成過程進行深刻分析以前,咱們先來看一下與自適應拓展息息相關的一個註解,即 Adaptive 註解。該註解的定義以下:學習

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

從上面的代碼中可知,Adaptive 可註解在類或方法上。註解在類上時,Dubbo 不會爲該類生成代理類。註解上方法(接口方法)上時,Dubbo 會爲爲該方法生成代理邏輯。Adaptive 註解在類上的狀況不多,在 Dubbo 中,僅有兩個類被 Adaptive 註解了,分別是 AdaptiveCompiler 和 AdaptiveExtensionFactory。此種狀況表示拓展的加載邏輯由人工編碼完成。更多時候,Adaptive 是註解在接口方法上的,表示拓展的加載邏輯需由框架自動生成。Adaptive 註解的地方不一樣,相應的處理邏輯也是不一樣的。註解在類上時,處理邏輯比較簡單,本文就不分析了。註解在接口方法上時,處理邏輯較爲複雜,本章將會重點分析此塊邏輯。接下來,咱們從 getAdaptiveExtension 方法進行分析。代碼以下:

2.1 獲取自適應拓展

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("...");
                    }
                }
            }
        } else {
            throw new IllegalStateException("...");
        }
    }

    return (T) instance;
}

getAdaptiveExtension 方法首先會檢查緩存,緩存未命中,則調用 createAdaptiveExtension 方法建立自適應拓展。下面,咱們看一下 createAdaptiveExtension 方法的代碼。

private T createAdaptiveExtension() {
    try {
        // 獲取自適應拓展類,並經過反射實例化
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
        throw new IllegalStateException("...");
    }
}

createAdaptiveExtension 方法代碼比較少,但卻包含了三個動做,分別以下:

  1. 調用 getAdaptiveExtensionClass 方法獲取自適應拓展 Class 對象
  2. 經過反射進行實例化
  3. 調用 injectExtension 方法向拓展實例中注入依賴

前兩個動做比較好理解,第三個動做很差理解,這裏簡單說明一下。injectExtension 方法經過 setter 方法向目標對象中注入依賴,能夠看作是一個簡單 IOC 的實現。前面說過,Dubbo 中有兩種類型的自適應拓展,一種是手工編碼的,一種是自動生成的。手工編碼的 Adaptive 拓展中可能存在着一些依賴,而自動生成的 Adaptive 拓展則不會依賴其餘類。這裏調用 injectExtension 方法的目的是爲手工編碼的自適應拓展注入依賴,這一點須要你們注意一下。關於 injectExtension 方法,我在[上一篇文章]()中已經分析過了,這裏再也不贅述。接下來,分析 getAdaptiveExtensionClass 方法的邏輯。

private Class<?> getAdaptiveExtensionClass() {
    // 經過 SPI 獲取全部的拓展類
    getExtensionClasses();
    // 檢查緩存,若緩存不爲空,則直接返回緩存
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    // 建立自適應拓展類
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

getAdaptiveExtensionClass 方法也包含了三個步驟,以下:

  1. 調用 getExtensionClasses 獲取全部的拓展類
  2. 檢查緩存,若緩存不爲空,則返回緩存
  3. 若緩存爲空,則調用 createAdaptiveExtensionClass 建立自適應拓展類

這三個步驟看起來平淡無奇,彷佛沒有多講的必要。可是這些平淡無奇的代碼中隱藏了一些細節,須要說明一下。首先從第一個步驟提及,getExtensionClasses 這個方法用於獲取某個接口的全部實現類。好比該方法能夠獲取 Protocol 接口的 DubboProtocol、HttpProtocol、InjvmProtocol 等實現類。在獲取實現類的過程當中,若是某個某個實現類被 Adaptive 註解修飾了,那麼該類就會被賦值給 cachedAdaptiveClass 變量。此時,上面步驟中的第二步條件成立(緩存不爲空),直接返回 cachedAdaptiveClass 便可。若是全部的實現類均未被 Adaptive 註解修飾,那麼執行第三步邏輯,建立自適應拓展類。相關代碼以下:

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

createAdaptiveExtensionClass 方法用於生成自適應拓展類,該方法首先會生成自適應拓展類的源碼,而後經過 Compiler 實例(Dubbo 默認使用 javassist 做爲編譯器)編譯源碼,獲得代理類 Class 實例。接下來,我將重點分析代理類代碼生成邏輯。至於代碼編譯的過程,並不是本文範疇,這裏就不分析了,你們有興趣能夠本身看看。下面,咱們把目光聚焦在 createAdaptiveExtensionClassCode 方法上。

2.2 自適應拓展類代碼生成

createAdaptiveExtensionClassCode 方法代碼略多,約有兩百行代碼。所以在本節中,我將會對該方法的代碼進行拆分分析,以幫助你們更好的理解代碼含義。

2.2.1 Adaptive 註解檢測

在生成代理類源碼以前,createAdaptiveExtensionClassCode 方法首先會經過反射檢測接口方法是否包含 Adaptive 註解。對於要生成自適應拓展的接口,Dubbo 要求該接口至少有一個方法被 Adaptive 註解修飾。若不知足此條件,就會拋出運行時異常。相關代碼以下:

// 經過反射獲取全部的方法
Method[] methods = type.getMethods();
boolean hasAdaptiveAnnotation = false;
// 遍歷方法列表
for (Method m : methods) {
    // 檢測方法上是否有 Adaptive 註解
    if (m.isAnnotationPresent(Adaptive.class)) {
        hasAdaptiveAnnotation = true;
        break;
    }
}

if (!hasAdaptiveAnnotation)
    // 若全部的方法上均無 Adaptive 註解,則拋出異常
    throw new IllegalStateException("...");

2.2.2 生成類

經過 Adaptive 註解檢測後,便可開始生成代碼。代碼生成的順序與 Java 文件內容順序一致,首先會生成 package 語句,而後生成 import 語句,緊接着生成類名等代碼。整個邏輯以下:

// 生成 package 代碼:package + type 所在包
codeBuilder.append("package ").append(type.getPackage().getName()).append(";");
// 生成 import 代碼:import + ExtensionLoader 全限定名
codeBuilder.append("\nimport ").append(ExtensionLoader.class.getName()).append(";");
// 生成類代碼:public class + type簡單名稱 + $Adaptive + implements + type全限定名 + {
codeBuilder.append("\npublic class ")
    .append(type.getSimpleName())
    .append("$Adaptive")
    .append(" implements ")
    .append(type.getCanonicalName())
    .append(" {");

// ${生成方法}

codeBuilder.append("\n}");

這裏,我用 ${...} 佔位符表明其餘代碼的生成邏輯,該部分邏輯我將在隨後進行分析。上面代碼不是很難理解,這裏我直接經過一個例子展現該段代碼所生成的內容。以 Dubbo 的 Protocol 接口爲例,生成的代碼以下:

package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol {
    // 省略方法代碼
}

2.2.3 生成方法

一個方法能夠被 Adaptive 註解修飾,也能夠不被修飾。這裏將未被 Adaptive 註解修飾的方法稱爲「無 Adaptive 註解方法」,下面咱們先來看看此種方法的代碼生成邏輯是怎樣的。

2.2.3.1 無 Adaptive 註解方法代碼生成

對於接口方法,咱們能夠按照需求標註 Adaptive 註解。以 Protocol 接口爲例,該接口的 destroy 和 getDefaultPort 未標註 Adaptive 註解,其餘方法均標註了 Adaptive 註解。Dubbo 不會爲沒有標註 Adaptive 註解的方法生成代理邏輯,對於該種類型的方法,僅會生成一句拋出異常的代碼。生成邏輯以下:

for (Method method : methods) {
    
    // 省略無關邏輯

    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    StringBuilder code = new StringBuilder(512);
    // 若是方法上無 Adaptive 註解,則生成 throw new UnsupportedOperationException(...) 代碼
    if (adaptiveAnnotation == null) {
        // 生成規則:
        // throw new UnsupportedOperationException(
        //     "method " + 方法簽名 + of interface + 全限定接口名 + is not adaptive method!」)
        code.append("throw new UnsupportedOperationException(\"method ")
            .append(method.toString()).append(" of interface ")
            .append(type.getName()).append(" is not adaptive method!\");");
    } else {
        // 省略無關邏輯
    }
    
    // 省略無關邏輯
}

以 Protocol 接口的 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!");
2.2.3.2 獲取 URL 數據

前面說過方法代理邏輯會從 URL 中提取目標拓展的名稱,所以代碼生成邏輯的一個重要的任務是從方法的參數列表獲取其餘參數中獲取 URL 數據。舉個例子說明一下,咱們要爲 Protocol 接口的 refer 和 export 方法生成代理邏輯。在運行時,經過反射獲得的方法定義大體以下:

Invoker refer(Class<T> arg0, URL arg1) throws RpcException;
Exporter export(Invoker<T> arg0) throws RpcException;

對於 refer 方法,經過遍歷 refer 的參數列表便可獲取 URL 數據,這個還比較簡單。對於 export 方法,獲取 URL 數據則要麻煩一些。export 參數列表中沒有 URL 參數,所以須要從 Invoker 參數中獲取 URL 數據。獲取方式是調用 Invoker 中可返回 URL 的 getter 方法,好比 getUrl。若是 Invoker 中無相關 getter 方法,此時則會拋出異常。整個邏輯以下:

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 註解方法代碼生成}
    } else {
        int urlTypeIndex = -1;
        // 遍歷參數列表,肯定 URL 參數位置
        for (int i = 0; i < pts.length; ++i) {
            if (pts[i].equals(URL.class)) {
                urlTypeIndex = i;
                break;
            }
        }
        if (urlTypeIndex != -1) {    // 參數列表中存在 URL 參數
            // 爲 URL 類型參數生成判空代碼,格式以下:
            // if (arg + urlTypeIndex == null) 
            //     throw new IllegalArgumentException("url == null");
            String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"url == null\");",
                                     urlTypeIndex);
            code.append(s);

            // 爲 URL 類型參數生成賦值代碼,即 URL url = arg1 或 arg2,或 argN
            s = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex);
            code.append(s);
            
        } else {    // 參數列表中不存在 URL 類型參數
            String attribMethod = null;

            LBL_PTS:
            // 遍歷方法的參數類型列表
            for (int i = 0; i < pts.length; ++i) {
                // 獲取某一類型參數的所有方法
                Method[] ms = pts[i].getMethods();
                // 遍歷方法列表,尋找可返回 URL 的 getter 方法
                for (Method m : ms) {
                    String name = m.getName();
                    // 1. 方法名以 get 開頭,或方法名大於3個字符
                    // 2. 方法的訪問權限爲 public
                    // 3. 方法非靜態類型
                    // 4. 方法參數數量爲0
                    // 5. 方法返回值類型爲 URL
                    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;
                        
                        // 結束 for (int i = 0; i < pts.length; ++i) 循環
                        break LBL_PTS;
                    }
                }
            }
            if (attribMethod == null) {
                // 若是全部參數中均不包含可返回 URL 的 getter 方法,則拋出異常
                throw new IllegalStateException("...");
            }

            // 爲包含可返回 URL 的參數生成判空代碼,格式以下:
            // if (arg + urlTypeIndex == null) 
            //     throw new IllegalArgumentException("參數全限定名 + argument == null");
            String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");",
                                     urlTypeIndex, pts[urlTypeIndex].getName());
            code.append(s);

            // 爲 getter 方法返回的 URL 生成判空代碼,格式以下:
            // if (argN.getter方法名() == null) 
            //     throw new IllegalArgumentException(參數全限定名 + argument getUrl() == null);
            s = String.format("\nif (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");",
                              urlTypeIndex, attribMethod, pts[urlTypeIndex].getName(), attribMethod);
            code.append(s);

            // 生成賦值語句,格式以下:
            // URL全限定名 url = argN.getter方法名(),好比 
            // com.alibaba.dubbo.common.URL url = invoker.getUrl();
            s = String.format("%s url = arg%d.%s();", URL.class.getName(), urlTypeIndex, attribMethod);
            code.append(s);
        }
        
        // 省略無關代碼
    }
    
    // 省略無關代碼
}

上面代碼有點多,但並非很難看懂。這段代碼主要是爲了獲取 URL 數據,併爲之生成判空和賦值代碼。以 Protocol 的 refer 和 export 方法爲例,上面代碼會爲它們生成以下內容(代碼已格式化):

refer:
if (arg1 == null) 
    throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg1;

export:
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();
2.2.3.3 獲取 Adaptive 註解值

Adaptive 註解值 value 類型爲 String[],可填寫多個值,默認狀況下爲空數組。若 value 爲非空數組,直接獲取數組內容便可。若 value 爲空數組,則需進行額外處理。處理的過程是將類名轉換爲字符數組,而後遍歷字符數組,並將字符加入到 StringBuilder 中。若字符爲大寫字母,則向 StringBuilder 中添加點號,隨後將字符變爲小寫存入 StringBuilder 中。好比 LoadBalance 通過處理後,獲得 load.balance。

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 註解方法代碼生成}
    } else {
        // ${獲取 URL 數據}
        
        String[] value = adaptiveAnnotation.value();
        // value 爲空數組
        if (value.length == 0) {
            // 獲取類名,並將類名轉換爲字符數組
            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 中添加點號
                        sb.append(".");
                    }
                    // 將字符變爲小寫,並添加到 sb 中
                    sb.append(Character.toLowerCase(charArray[i]));
                } else {
                    // 添加字符到 sb 中
                    sb.append(charArray[i]);
                }
            }
            value = new String[]{sb.toString()};
        }
        
        // 省略無關代碼
    }
    
    // 省略無關邏輯
}
2.2.3.4 檢測 Invocation 參數

此段邏輯是檢測方法列表中是否存在 Invocation 類型的參數,若存在,則爲其生成判空代碼和其餘一些代碼。相應的邏輯以下:

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 註解方法代碼生成}
    } else {
        // ${獲取 URL 數據}
        
        // ${獲取 Adaptive 註解值}
        
        boolean hasInvocation = false;
        // 遍歷參數類型列表
        for (int i = 0; i < pts.length; ++i) {
            // 判斷當前參數名稱是否等於 com.alibaba.dubbo.rpc.Invocation
            if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) {
                // 爲 Invocation 類型參數生成判空代碼
                String s = String.format("\nif (arg%d == null) throw new IllegalArgumentException(\"invocation == null\");", i);
                code.append(s);
                // 生成 getMethodName 方法調用代碼,格式爲:
                //    String methodName = argN.getMethodName();
                s = String.format("\nString methodName = arg%d.getMethodName();", i);
                code.append(s);
                
                // 設置 hasInvocation 爲 true
                hasInvocation = true;
                break;
            }
        }
    }
    
    // 省略無關邏輯
}
2.2.3.5 生成拓展名獲取邏輯

本段邏輯用於根據 SPI 和 Adaptive 註解值生成「拓展名獲取邏輯」,同時生成邏輯也受 Invocation 類型參數影響,綜合因素致使本段邏輯相對複雜。本段邏輯能夠會生成但不限於下面的代碼:

String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());

String extName = url.getMethodParameter(methodName, "loadbalance", "random");

亦或是

String extName = url.getParameter("client", url.getParameter("transporter", "netty"));

本段邏輯複雜指出在於條件分支比較多,你們在閱讀源碼時須要知道每一個條件分支的意義是什麼,不然不太容易看懂相關代碼。好了,其餘的就很少說了,開始分析本段邏輯。

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 註解方法代碼生成}
    } else {
        // ${獲取 URL 數據}
        
        // ${獲取 Adaptive 註解值}
        
        // ${檢測 Invocation 參數}
        
        // 設置默認拓展名,cachedDefaultName = SPI 註解值,好比 Protocol 接口上標註的 
        // SPI 註解值爲 dubbo。默認狀況下,SPI 註解值爲空串,此時 cachedDefaultName = null
        String defaultExtName = cachedDefaultName;
        String getNameCode = null;
        
        // 遍歷 value,這裏的 value 是 Adaptive 的註解值,2.2.3.3 節分析過 value 變量的獲取過程。
        // 此處循環目的是生成從 URL 中獲取拓展名的代碼,生成的代碼會賦值給 getNameCode 變量。注意這
        // 個循環的遍歷順序是由後向前遍歷的。
        for (int i = value.length - 1; i >= 0; --i) {
            if (i == value.length - 1) {    // 當 i 爲最後一個元素的座標時
                if (null != defaultExtName) {   // 默認拓展名非空
                    // protocol 是 url 的一部分,可經過 getProtocol 方法獲取,其餘的則是從
                    // URL 參數中獲取。因此這裏要判斷 value[i] 是否爲 protocol
                    if (!"protocol".equals(value[i]))
                        // hasInvocation 用於標識方法參數列表中是否有 Invocation 類型參數
                        if (hasInvocation)
                            // 生成的代碼功能等價於下面的代碼:
                            //   url.getMethodParameter(methodName, value[i], defaultExtName)
                            // 以 LoadBalance 接口的 select 方法爲例,最終生成的代碼以下:
                            //   url.getMethodParameter(methodName, "loadbalance", "random")
                            getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                        else
                            // 生成的代碼功能等價於下面的代碼:
                            //   url.getParameter(value[i], defaultExtName)
                            getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
                    else
                        // 生成的代碼功能等價於下面的代碼:
                        //   ( url.getProtocol() == null ? defaultExtName : url.getProtocol() )
                        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
                            // 生成的代碼功能等價於下面的代碼:
                            //   url.getParameter(value[i])
                            getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
                    else
                        // 生成從 url 中獲取協議的代碼,好比 "dubbo"
                        getNameCode = "url.getProtocol()";
                }
            } else {
                if (!"protocol".equals(value[i]))
                    if (hasInvocation)
                        // 生成代碼格式同上
                        getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
                    else
                        // 生成的代碼功能等價於下面的代碼:
                        //   url.getParameter(value[i], getNameCode)
                        // 以 Transporter 接口的 connect 方法爲例,最終生成的代碼以下:
                        //   url.getParameter("client", url.getParameter("transporter", "netty"))
                        getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
                else
                    // 生成的代碼功能等價於下面的代碼:
                    //   url.getProtocol() == null ? getNameCode : url.getProtocol()
                    // 以 Protocol 接口的 connect 方法爲例,最終生成的代碼以下:
                    //   url.getProtocol() == null ? "dubbo" : url.getProtocol()
                    getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
            }
        }
        // 生成 extName 賦值代碼
        code.append("\nString extName = ").append(getNameCode).append(";");
        // 生成 extName 判空代碼
        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);
    }
    
    // 省略無關邏輯
}

上面代碼已經進行了大量的註釋,不過看起來任然不是很好理解。既然如此,那麼建議你們寫點測試代碼,對 Protocol、LoadBalance 以及 Transporter 等接口的自適應拓展類代碼生成過程進行調試。這裏我以 Transporter 接口的自適應拓展類代碼生成過程進行分析。首先看一下 Transporter 接口的定義,以下:

@SPI("netty")
public interface Transporter {
    // @Adaptive({server, transporter})
    @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY}) 
    Server bind(URL url, ChannelHandler handler) throws RemotingException;

    // @Adaptive({client, transporter})
    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    Client connect(URL url, ChannelHandler handler) throws RemotingException;
}

下面對 connect 方法代理邏輯生成的過程進行分析,此時生成代理邏輯所用到的變量和值以下:

String defaultExtName = "netty";
boolean hasInvocation = false;
String getNameCode = null;
String[] value = ["client", "transporter"];

下面對 value 數組進行遍歷,此時 i = 1, value[i] = "transporter",生成的代碼以下:

getNameCode = url.getParameter("transporter", "netty");

接下來,for 循環繼續執行,此時 i = 0, value[i] = "client",生成的代碼以下:

getNameCode = url.getParameter("client", url.getParameter("transporter", "netty"));

for 循環結束運行,如今生成 extName 變量及判空代碼,以下:

String extName = url.getParameter("client", url.getParameter("transporter", "netty"));
if (extName == null) {
    throw new IllegalStateException(
        "Fail to get extension(com.alibaba.dubbo.remoting.Transporter) name from url(" + url.toString()
        + ") use keys([client, transporter])");
}

到此,connect 方法的拓展名獲取代碼就生成好了。若是你們不是很明白,建議本身調試走一遍。好了,本節先到這裏。

2.2.3.6 生成拓展加載與目標方法調用邏輯

上一節的邏輯生成拓展名 extName 獲取邏輯,接下來要作的是根據拓展名加載拓展實例,並調用拓展實例的目標方法。相關邏輯以下:

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 註解方法代碼生成}
    } else {
        // ${獲取 URL 數據}
        
        // ${獲取 Adaptive 註解值}
        
        // ${檢測 Invocation 參數}
        
        // ${生成拓展名獲取邏輯}
        
        // 生成拓展獲取代碼,格式以下:
        // type全限定名 extension = (type全限定名)ExtensionLoader全限定名
        //     .getExtensionLoader(type全限定名.class).getExtension(extName);
        // Tips: 格式化字符串中的 %<s 表示使用前一個轉換符所描述的參數,即 type 全限定名
        s = String.format("\n%s extension = (%<s)%s.getExtensionLoader(%s.class).getExtension(extName);",
                        type.getName(), ExtensionLoader.class.getSimpleName(), type.getName());
        code.append(s);

        // 若是方法有返回值類型非 void,則生成 return 語句。
        if (!rt.equals(void.class)) {
            code.append("\nreturn ");
        }

        // 生成目標方法調用邏輯,格式爲:
        //     extension.方法名(arg0, arg2, ..., argN);
        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(");");   
    }
    
    // 省略無關邏輯
}

以 Protocol 接口舉例說明,上面代碼生成的內容以下:

com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader
            .getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
return extension.refer(arg0, arg1);
2.2.3.7 生成完整的方法

本節進行代碼生成的收尾工做,主要用於生成方法定義的代碼。相關邏輯以下:

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 註解方法代碼生成}
    } else {
        // ${獲取 URL 數據}
        
        // ${獲取 Adaptive 註解值}
        
        // ${檢測 Invocation 參數}
        
        // ${生成拓展名獲取邏輯}
        
        // ${生成拓展加載與目標方法調用邏輯}
    }
}
    
// public + 返回值全限定名 + 方法名 + (
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}");

以 Protocol 的 refer 方法爲例,上面代碼生成的內容以下:

public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) {
    // 方法體
}

3.基於動態代理實現知識與拓展

我在第一章介紹自適應拓展原理時說過,Dubbo 經過生成和編譯代碼實現自適應拓展的方式有點複雜,不利於維護。另外,這樣作對源碼學習讀者來講,也不是很友好。我敢確定,有同窗會像我同樣,在開始調試 Dubbo 源碼時,不知道如何調試各類自適應拓展類,好比 Protocol$Adaptive。若是你也有相似的困惑,這裏教你們一個方法。以下:

  1. 在 createAdaptiveExtensionClass 方法的第一行打個斷點
  2. 啓動測試代碼,代碼運行到端點處,單步越過斷點,此時能夠獲得生成的代碼。
  3. 拷貝出剛剛獲取到的代碼,到指定的包下建立同名類,並將代碼拷過去,格式化一下便可

以 Protocol 接口爲例,當代碼越過斷點後,調試信息以下:

從調試信息中可知,Protocol$Adaptive 所在包爲 com.alibaba.dubbo.rpc。所以接下來到 com.alibaba.dubbo.rpc 包下建立 Protocol$Adaptive 類,並把 code 變量值拷貝到剛建立的文件中。當咱們再次進行調試時,就能進入內部了。好比:

既然 Dubbo 實現的 Adaptive 機制不利於調試,那麼咱們能夠對其進行改造。改造後的代碼以下:

public class AdaptiveInvokeHandler implements InvocationHandler {

    private String defaultExtName;

    public AdaptiveInvokeHandler(String defaultExtName) {
        this.defaultExtName = defaultExtName;
    }

    public Object getProxy(Class clazz) {
        if (!clazz.isInterface()) {
            throw new IllegalStateException("Only create the proxy for interface.");
        }
        return Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{clazz}, this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Class<?> type = method.getDeclaringClass();
        if (type.equals(Object.class)) {
            throw new UnsupportedOperationException("Cannot invoke the method of Object");
        }
        Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
        if (adaptiveAnnotation == null) {
            throw new UnsupportedOperationException("method " + method.toString() + " of interface " + type.getName() + " is not adaptive method!");
        }

        // 獲取 URL 數據
        URL url = getUrlData(method, args);
        // 獲取 Adaptive 註解值
        String[] value = getAdaptiveAnnotationValue(method);
        // 獲取 Invocation 參數
        Object invocation = getInvocationArgument(method, args);

        // 獲取拓展名
        String extName = getExtensionName(url, value, invocation);
        if (StringUtils.isEmpty(extName)) {
            throw new IllegalStateException(
                "Fail to get extension(" + type.getName() + ") name from url(" + url.toString()
                    + ") use keys(" + Arrays.toString(value) +")");
        }

        // 獲取拓展實例
        Object extension = ExtensionLoader.getExtensionLoader(type).getExtension(extName);
        Class<?> extType = extension.getClass();
        Method targetMethod = extType.getMethod(method.getName(), method.getParameterTypes());
        // 經過反射調用目標方法
        return targetMethod.invoke(extension, args);
    }
}

這樣看起來是否是簡單了一些,不過這並非所有的代碼。我將 URL 數據以及 Adaptive 註解值的獲取邏輯封裝在了私有方法中,相應的代碼以下:

private URL getUrlData(Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
    URL url = null;
    Class<?>[] pts = method.getParameterTypes();
    for (int i = 0; i < pts.length; i++) {
        if (pts[i].equals(URL.class)) {
            url = (URL) args[i];
            if (url == null) {
                throw new IllegalArgumentException("url == null");
            }
            break;
        }
    }

    if (url == null) {
        int urlTypeIndex = -1;
        Method getter = null;

        LBL_PTS:
        for (int i = 0; i < pts.length; ++i) {
            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;
                    getter = m;
                    break LBL_PTS;
                }
            }
        }

        if (urlTypeIndex == -1) {
            throw new IllegalArgumentException("Cannot find URL argument.");
        }

        if (args[urlTypeIndex] == null) {
            throw new IllegalArgumentException(pts[urlTypeIndex].getName() + " argument == null");
        }

        url = (URL) getter.invoke(args[urlTypeIndex]);
        if (url == null) {
            throw new IllegalArgumentException(pts[urlTypeIndex].getName() + " argument " + getter.getName() + "() == null");
        }
    }

    return url;
}

private String[] getAdaptiveAnnotationValue(Method method) {
    Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
    Class type = method.getDeclaringClass();
    if (adaptiveAnnotation == null) {
        throw new IllegalArgumentException("method " + method.toString() + " of interface " + type.getName() + " is not adaptive method!");
    }

    String[] value = adaptiveAnnotation.value();
    if (value.length == 0) {
        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()};
    }

    return value;
}

private Object getInvocationArgument(Method method, Object[] args) {
    Class<?>[] pts = method.getParameterTypes();
    for (int i = 0; i < pts.length; ++i) {
        if (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) {
            Object invocation = args[i];
            if (invocation == null) {
                throw new IllegalArgumentException("invocation == null");
            }
            return invocation;
        }
    }

    return null;
}

private String getExtensionName(URL url, String[] value, Invocation invocation) {
    String methodName = null;
    boolean hasInvocation = invocation != null;
    if (hasInvocation) {
        Class<?> clazz = invocation.getClass();
        Method method = clazz.getMethod("getMethodName");
        methodName = (String) method.invoke(invocation);
    }
    String extName = null;
    for (int i = 0; i < value.length; i++) {
        if (!"protocol".equals(value[i])) {
            if (hasInvocation) {
                extName = url.getMethodParameter(methodName, value[i], defaultExtName);
            } else {
                extName = url.getParameter(value[i]);
            }
        } else {
            extName = url.getProtocol();
        }

        if (StringUtils.isNotEmpty(extName)) {
            break;
        }

        if (i == value.length -1 && StringUtils.isEmpty(extName)) {
            extName = defaultExtName;
        }
    }

    return extName;
}

如今咱們將 AdaptiveInvokeHandler 放置到 ExtensionLoader 所在包下,並對 ExtensionLoader 的 createAdaptiveExtension 方法代碼進行改造。以下:

private T createAdaptiveExtension() {
    try {
        getExtensionClasses();
        T extension = null;
        if (cachedAdaptiveClass != null) {
            extension = (T) cachedAdaptiveClass.newInstance();
        }
        if (extension == null) {
            extension = (T) new AdaptiveInvokeHandler(cachedDefaultName).getProxy(type);
        }

        return injectExtension(extension);
    } catch (Exception e) {
        throw new IllegalStateException("Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e);
    }
}

以上就是改造後的代碼,須要特別說明的是,上面的代碼僅供演示使用,代碼邏輯並非十分嚴謹。若是你有更好的寫法,歡迎分享。

4.總結

到此,關於自適應拓展的原理,實現以及改造過程就分析完了。總的來講自適應拓展整個邏輯仍是很複雜的,並非很容易弄懂。所以,你們在閱讀該部分源碼時,耐心一些,同時多進行調試。亦或是經過生成好的代碼思考生成邏輯。固然,你們也能夠將代碼生成邏輯當作一個黑盒,不懂細節也不要緊,只要知道自適應拓展原理便可。

好了,本篇文章先到這裏,感謝你們的閱讀。

本文在知識共享許可協議 4.0 下發布,轉載需在明顯位置處註明出處
做者:田小波
本文同步發佈在個人我的博客: http://www.tianxiaobo.com

cc
本做品採用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議進行許可。

相關文章
相關標籤/搜索