Dubbo 源碼分析 - 自適應拓展原理
1.原理
我在上一篇文章中分析了 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 注解。該注解的定義如下:
@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 方法代碼比較少,但卻包含了三個動作,分別如下:
- 調用 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 參數
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

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

浙公網安備 33010602011771號