1. 程式人生 > >Dubbo 原始碼分析 - 自適應北京PK10原始碼出售拓展原理

Dubbo 原始碼分析 - 自適應北京PK10原始碼出售拓展原理

1.原理
我在上一篇文章北京PK10原始碼出售 QQ2952777280【話仙原始碼論壇】hxforum.com 分析了 Dubbo 的 SPI 機制,Dubbo SPI 是 Dubbo 框架的核心。Dubbo 中的很多拓展都是通過 SPI 機制進行載入的,比如 Protocol、Cluster、LoadBalance 等。有時,有些拓展並非想在框架啟動階段被載入,而是希望在拓展方法被呼叫時,根據執行時引數進行載入。這聽起來有些矛盾。拓展未被載入,那麼拓展方法就無法被呼叫(靜態方法除外)。拓展方法未被呼叫,就無法進行載入,這似乎是個死結。不過好在也有相應的解決辦法,通過代理模式就可以解決這個問題,這裡我們將具有代理功能的拓展稱之為自適應拓展。Dubbo 並未直接通過代理模式實現自適應拓展,而是代理代理模式基礎上,封裝了一個更炫的實現方式。Dubbo 首先會為拓展介面生成具有代理功能的程式碼,然後通過 javassist 或 jdk 編譯這段程式碼,得到 Class 類,最後在通過反射建立代理類。整個過程比較複雜、炫麗,但有炫技的嫌疑。如此複雜的過程最終的目的是為拓展生成代理物件,但實際上每個代理物件的代理邏輯基本一致,均是從 URL 中獲取要載入的具體實現類。因此,我們完全可以把代理邏輯抽出來,並通過動態代理的方式實現自適應拓展。這樣做的好處顯而易見,方便維護,也方便原始碼學習者學習和除錯程式碼。本文將在隨後實現一個動態代理版的自適應拓展,有興趣的同學可以繼續往下讀。

接下來,我們通過一個示例演示自適應拓展類。這個示例取自 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 是一個代理類,它主要做了三件事情:

從 URL 中獲取 WheelMaker 名稱
通過 SPI 載入具體的 WheelMaker
呼叫目標方法
接下來,我們來看看汽車製造廠 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 型別的引數:

dubbo://192.168.0.101:20880/XxxService?wheel.maker=MichelinWheelMaker
RaceCarMaker 的 makeCar 方法將上面的 url 作為引數傳給 AdaptiveWheelMaker 的 makeWheel 方法,makeWheel 方法從 url 中提取 wheel.maker 引數,得到 MichelinWheelMaker。之後再通過 SPI 載入名為 MichelinWheelMaker 的實現類,得到具體的 WheelMaker 例項。

上面這個示例展示了自適應拓展類的核心實現 -- 在元件方法被呼叫時,通過代理的方式載入指定的實現類,並呼叫被代理的方法。

經過以上說明,大家應該搞懂了自適應拓展的原理。接下來,我們深入到原始碼中,探索自適應拓展生成的過程。

2.原始碼分析
在對自適應拓展生成過程進行深入分析之前,我們先來看一下與自適應拓展息息相關的一個註解,即 Adaptive 註解。該註解的定義如下:

@Documentedbr/>@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 方法程式碼比較少,但卻包含了三個動作,分別如下:

呼叫 getAdaptiveExtensionClass 方法獲取自適應拓展 Class 物件
通過反射進行例項化
呼叫 injectExtension 方法向拓展例項中注入依賴
前兩個動作比較好理解,第三個動作不好理解,這裡簡單說明一下。injectExtension 方法通過 setter 方法向目標物件中注入依賴,可以看做是一個簡單 IOC 的實現。前面說過,Dubbo 中有兩種型別的自適應拓展,一種是手工編碼的,一種是自動生成的。手工編碼的 Adaptive 拓展中可能存在著一些依賴,而自動生成的 Adaptive 拓展則不會依賴其他類。這裡呼叫 injectExtension 方法的目的是為手工編碼的自適應拓展注入依賴,這一點需要大家注意一下。關於 injectExtension 方法,我在上一篇文章中已經分析過了,這裡不再贅述。接下來,分析 getAdaptiveExtensionClass 方法的邏輯。

private Class<?> getAdaptiveExtensionClass() {
// 通過 SPI 獲取所有的拓展類
getExtensionClasses();
// 檢查快取,若快取不為空,則直接返回快取
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
// 建立自適應拓展類
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
getAdaptiveExtensionClass 方法也包含了三個步驟,如下:

呼叫 getExtensionClasses 獲取所有的拓展類
檢查快取,若快取不為空,則返回快取
若快取為空,則呼叫 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。如果你也有類似的困惑,這裡教大家一個方法。如下:

在 createAdaptiveExtensionClass 方法的第一行打個斷點
啟動測試程式碼,程式碼執行到端點處,單步越過斷點,此時可以得到生成的程式碼。
拷貝出剛剛獲取到的程式碼,到指定的包下建立同名類,並將程式碼拷過去,格式化一下即可
以 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 引數
Invocation 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 Invocation getInvocationArgument(Method method, Object[] args) {
Class<?>[] pts = method.getParameterTypes();
for (int i = 0; i < pts.length; ++i) {
if (pts[i].equals(Invocation.class)) {
Invocation invocation = (Invocation) args[i];
if (invocation == null) {
throw new IllegalArgumentException("invocation == null");
}
return invocation;
}
}

return null;
}

private String getExtensionName(URL url, String[] value, Invocation invocation) {
boolean hasInvocation = invocation != null;
String methodName = hasInvocation ? invocation.getMethodName() : null;
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);
}

}
要讓改造後的程式碼跑起來,還需要進行一些操作。AdaptiveInvokeHandler 所在的模組為 dubbo-common,該模組作為公共模組,不會引用 Dubbo 的其他模組。但 AdaptiveInvokeHandler 中用到了 dubbo-rpc-api 模組下的 Invocation 介面,因此我們要修改 AdaptiveInvokeHandler 的程式碼,將 Invocation 移除掉,保證程式碼通過編譯。這裡不能讓 dubbo-common 引入 dubbo-rpc-api,會導致迴圈依賴錯誤。

需要特別說明一下,上面的程式碼僅供演示使用。限於個人能力,部分程式碼邏輯可能會不嚴謹,如果你有更好的寫法,歡迎分享。

4.總結
到此,關於自適應拓展的原理,實現以及改造過程就分析完了。總的來說自適應拓展整個邏輯還是很複雜的,並不是很容易弄懂。因此,大家在閱讀該部分原始碼時,耐心一些,同時多進行除錯。亦或是通過生成好的程式碼思考生成邏輯。當然,大家也可以將程式碼生成邏輯看成一個黑盒,不懂細節也沒關係,只要知道自適應拓展原理即可。