Dubbo 源码分析 - 自适应拓展原理
1.原理
我在上一篇文章中分析了 Dubbo 的 SPI 機制,Dubbo SPI 是 Dubbo 框架的核心。Dubbo 中的很多拓展都是通過 SPI 機制進行加載的,比如 Protocol、Cluster、LoadBalance 等。有時,有些拓展并非想在框架啟動階段被加載,而是希望在拓展方法被調用時,根據(jù)運行時參數(shù)進行加載。這聽起來有些矛盾。拓展未被加載,那么拓展方法就無法被調用(靜態(tài)方法除外)。拓展方法未被調用,就無法進行加載,這似乎是個死結。不過好在也有相應的解決辦法,通過代理模式就可以解決這個問題,這里我們將具有代理功能的拓展稱之為自適應拓展。Dubbo 并未直接通過代理模式實現(xiàn)自適應拓展,而是代理代理模式基礎上,封裝了一個更炫的實現(xiàn)方式。Dubbo 首先會為拓展接口生成具有代理功能的代碼,然后通過 javassist 或 jdk 編譯這段代碼,得到 Class 類,最后在通過反射創(chuàng)建代理類。整個過程比較復雜、炫麗。如此復雜的過程最終的目的是為拓展生成代理對象,但實際上每個代理對象的代理邏輯基本一致,均是從 URL 中獲取欲加載實現(xiàn)類的名稱。因此,我們完全可以把代理邏輯抽出來,并通過動態(tài)代理的方式實現(xiàn)自適應拓展。這樣做的好處顯而易見,方便維護,也方便源碼學習者學習和調試代碼。本文將在隨后實現(xiàn)一個動態(tài)代理版的自適應拓展,有興趣的同學可以繼續(xù)往下讀。
接下來,我們通過一個示例演示自適應拓展類。這個示例取自 Dubbo 官方文檔,我這里進行了一定的拓展。這是一個與汽車相關的例子,我們有一個車輪制造廠接口 WheelMaker:
| 1 2 3 | public interface WheelMaker {Wheel makeWheel(URL url); } |
WheelMaker 接口的 Adaptive 實現(xiàn)類如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 加載具體的 WheelMakerWheelMaker wheelMaker = ExtensionLoader.getExtensionLoader(WheelMaker.class).getExtension(wheelMakerName);// 3.調用目標方法return wheelMaker.makeWheel(URL url);} } |
AdaptiveWheelMaker 是一個代理類,它主要做了三件事情:
接下來,我們來看看汽車制造廠 CarMaker 接口與其實現(xiàn)類。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public interface CarMaker {Car makeCar(URL url); }public class RaceCarMaker implements CarMaker {WheelMaker wheelMaker;// 通過 setter 注入 AdaptiveWheelMakerpublic 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 類型的參數(shù):
| 1 | dubbo://192.168.0.101:20880/XxxService?wheel.maker=MichelinWheelMaker |
RaceCarMaker 的 makeCar 方法將上面的 url 作為參數(shù)傳給 AdaptiveWheelMaker 的 makeWheel 方法,makeWheel 方法從 url 中提取 wheel.maker 參數(shù),得到 MichelinWheelMaker。之后再通過 SPI 加載名為 MichelinWheelMaker 的實現(xiàn)類,得到具體的 WheelMaker 實例。
上面這個示例展示了自適應拓展類的核心實現(xiàn) – 在組件方法被調用時,通過代理的方式加載指定的實現(xiàn)類,并調用被代理的方法。
經(jīng)過以上說明,大家應該搞懂了自適應拓展的原理。接下來,我們深入到源碼中,探索自適應拓展生成的過程。
?2.源碼分析
在對自適應拓展生成過程進行深入分析之前,我們先來看一下與自適應拓展息息相關的一個注解,即 Adaptive 注解。該注解的定義如下:
| 1 2 3 4 5 6 | @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 獲取自適應拓展
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public T getAdaptiveExtension() {// 從緩存中獲取自適應拓展Object instance = cachedAdaptiveInstance.get();if (instance == null) { // 緩存未命中if (createAdaptiveInstanceError == null) {synchronized (cachedAdaptiveInstance) {instance = cachedAdaptiveInstance.get();if (instance == null) {try {// 創(chuàng)建自適應拓展instance = createAdaptiveExtension();// 設置拓展到緩存中cachedAdaptiveInstance.set(instance);} catch (Throwable t) {createAdaptiveInstanceError = t;throw new IllegalStateException("...");}}}} else {throw new IllegalStateException("...");}}return (T) instance; } |
getAdaptiveExtension 方法首先會檢查緩存,緩存未命中,則調用 createAdaptiveExtension 方法創(chuàng)建自適應拓展。下面,我們看一下 createAdaptiveExtension 方法的代碼。
| 1 2 3 4 5 6 7 8 | private T createAdaptiveExtension() {try {// 獲取自適應拓展類,并通過反射實例化return injectExtension((T) getAdaptiveExtensionClass().newInstance());} catch (Exception e) {throw new IllegalStateException("...");} } |
createAdaptiveExtension 方法代碼比較少,但卻包含了三個動作,分別如下:
前兩個動作比較好理解,第三個動作不好理解,這里簡單說明一下。injectExtension 方法通過 setter 方法向目標對象中注入依賴,可以看做是一個簡單 IOC 的實現(xiàn)。前面說過,Dubbo 中有兩種類型的自適應拓展,一種是手工編碼的,一種是自動生成的。手工編碼的 Adaptive 拓展中可能存在著一些依賴,而自動生成的 Adaptive 拓展則不會依賴其他類。這里調用 injectExtension 方法的目的是為手工編碼的自適應拓展注入依賴,這一點需要大家注意一下。關于 injectExtension 方法,我在上一篇文章中已經(jīng)分析過了,這里不再贅述。接下來,分析 getAdaptiveExtensionClass 方法的邏輯。
| 1 2 3 4 5 6 7 8 9 10 | private Class<?> getAdaptiveExtensionClass() {// 通過 SPI 獲取所有的拓展類getExtensionClasses();// 檢查緩存,若緩存不為空,則直接返回緩存if (cachedAdaptiveClass != null) {return cachedAdaptiveClass;}// 創(chuàng)建自適應拓展類return cachedAdaptiveClass = createAdaptiveExtensionClass(); } |
getAdaptiveExtensionClass 方法也包含了三個步驟,如下:
這三個步驟看起來平淡無奇,似乎沒有多講的必要。但是這些平淡無奇的代碼中隱藏了一些細節(jié),需要說明一下。首先從第一個步驟說起,getExtensionClasses 這個方法用于獲取某個接口的所有實現(xiàn)類。比如該方法可以獲取 Protocol 接口的 DubboProtocol、HttpProtocol、InjvmProtocol 等實現(xiàn)類。在獲取實現(xiàn)類的過程中,如果某個某個實現(xiàn)類被 Adaptive 注解修飾了,那么該類就會被賦值給 cachedAdaptiveClass 變量。此時,上面步驟中的第二步條件成立(緩存不為空),直接返回 cachedAdaptiveClass 即可。如果所有的實現(xiàn)類均未被 Adaptive 注解修飾,那么執(zhí)行第三步邏輯,創(chuàng)建自適應拓展類。相關代碼如下:
| 1 2 3 4 5 6 7 8 9 | private Class<?> createAdaptiveExtensionClass() {// 構建自適應拓展代碼String code = createAdaptiveExtensionClassCode();ClassLoader classLoader = findClassLoader();// 獲取編譯器實現(xiàn)類com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();// 編譯代碼,生成 Classreturn compiler.compile(code, classLoader); } |
createAdaptiveExtensionClass 方法用于生成自適應拓展類,該方法首先會生成自適應拓展類的源碼,然后通過 Compiler 實例(Dubbo 默認使用 javassist 作為編譯器)編譯源碼,得到代理類 Class 實例。接下來,我將重點分析代理類代碼生成邏輯。至于代碼編譯的過程,并非本文范疇,這里就不分析了,大家有興趣可以自己看看。下面,我們把目光聚焦在 createAdaptiveExtensionClassCode 方法上。
?2.2 自適應拓展類代碼生成
createAdaptiveExtensionClassCode 方法代碼略多,約有兩百行代碼。因此在本節(jié)中,我將會對該方法的代碼進行拆分分析,以幫助大家更好的理解代碼含義。
?2.2.1 Adaptive 注解檢測
在生成代理類源碼之前,createAdaptiveExtensionClassCode 方法首先會通過反射檢測接口方法是否包含 Adaptive 注解。對于要生成自適應拓展的接口,Dubbo 要求該接口至少有一個方法被 Adaptive 注解修飾。若不滿足此條件,就會拋出運行時異常。相關代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 通過反射獲取所有的方法 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 文件內(nèi)容順序一致,首先會生成 package 語句,然后生成 import 語句,緊接著生成類名等代碼。整個邏輯如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 生成 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}"); |
這里,我用 ${…} 占位符代表其他代碼的生成邏輯,該部分邏輯我將在隨后進行分析。上面代碼不是很難理解,這里我直接通過一個例子展示該段代碼所生成的內(nèi)容。以 Dubbo 的 Protocol 接口為例,生成的代碼如下:
| 1 2 3 4 5 | 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 注解的方法生成代理邏輯,對于該種類型的方法,僅會生成一句拋出異常的代碼。生成邏輯如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | for (Method method : methods) {// 省略無關邏輯Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);StringBuilder code = new StringBuilder(512);// 如果方法上無 Adaptive 注解,則生成 throw new UnsupportedOperationException(...) 代碼if (adaptiveAnnotation == null) {// 生成規(guī)則:// 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 方法為例,上面代碼生成的內(nèi)容如下:
| 1 2 | 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 數(shù)據(jù)
前面說過方法代理邏輯會從 URL 中提取目標拓展的名稱,因此代碼生成邏輯的一個重要的任務是從方法的參數(shù)列表獲取其他參數(shù)中獲取 URL 數(shù)據(jù)。舉個例子說明一下,我們要為 Protocol 接口的 refer 和 export 方法生成代理邏輯。在運行時,通過反射得到的方法定義大致如下:
| 1 2 | Invoker refer(Class<T> arg0, URL arg1) throws RpcException; Exporter export(Invoker<T> arg0) throws RpcException; |
對于 refer 方法,通過遍歷 refer 的參數(shù)列表即可獲取 URL 數(shù)據(jù),這個還比較簡單。對于 export 方法,獲取 URL 數(shù)據(jù)則要麻煩一些。export 參數(shù)列表中沒有 URL 參數(shù),因此需要從 Invoker 參數(shù)中獲取 URL 數(shù)據(jù)。獲取方式是調用 Invoker 中可返回 URL 的 getter 方法,比如 getUrl。如果 Invoker 中無相關 getter 方法,此時則會拋出異常。整個邏輯如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | 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;// 遍歷參數(shù)列表,確定 URL 參數(shù)位置for (int i = 0; i < pts.length; ++i) {if (pts[i].equals(URL.class)) {urlTypeIndex = i;break;}}if (urlTypeIndex != -1) { // 參數(shù)列表中存在 URL 參數(shù)// 為 URL 類型參數(shù)生成判空代碼,格式如下:// 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 類型參數(shù)生成賦值代碼,即 URL url = arg1 或 arg2,或 argNs = String.format("\n%s url = arg%d;", URL.class.getName(), urlTypeIndex);code.append(s);} else { // 參數(shù)列表中不存在 URL 類型參數(shù)String attribMethod = null;LBL_PTS:// 遍歷方法的參數(shù)類型列表for (int i = 0; i < pts.length; ++i) {// 獲取某一類型參數(shù)的全部方法Method[] ms = pts[i].getMethods();// 遍歷方法列表,尋找可返回 URL 的 getter 方法for (Method m : ms) {String name = m.getName();// 1. 方法名以 get 開頭,或方法名大于3個字符// 2. 方法的訪問權限為 public// 3. 方法非靜態(tài)類型// 4. 方法參數(shù)數(shù)量為0// 5. 方法返回值類型為 URLif ((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) 循環(huán)break LBL_PTS;}}}if (attribMethod == null) {// 如果所有參數(shù)中均不包含可返回 URL 的 getter 方法,則拋出異常throw new IllegalStateException("...");}// 為包含可返回 URL 的參數(shù)生成判空代碼,格式如下:// if (arg + urlTypeIndex == null) // throw new IllegalArgumentException("參數(shù)全限定名 + 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(參數(shù)全限定名 + 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 數(shù)據(jù),并為之生成判空和賦值代碼。以 Protocol 的 refer 和 export 方法為例,上面代碼會為它們生成如下內(nèi)容(代碼已格式化):
| 1 2 3 4 5 6 7 8 9 10 11 | 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[],可填寫多個值,默認情況下為空數(shù)組。若 value 為非空數(shù)組,直接獲取數(shù)組內(nèi)容即可。若 value 為空數(shù)組,則需進行額外處理。處理的過程是將類名轉換為字符數(shù)組,然后遍歷字符數(shù)組,并將字符加入到 StringBuilder 中。若字符為大寫字母,則向 StringBuilder 中添加點號,隨后將字符變?yōu)樾懘嫒?StringBuilder 中。比如 LoadBalance 經(jīng)過處理后,得到 load.balance。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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 數(shù)據(jù)}String[] value = adaptiveAnnotation.value();// value 為空數(shù)組if (value.length == 0) {// 獲取類名,并將類名轉換為字符數(shù)組char[] charArray = type.getSimpleName().toCharArray();StringBuilder sb = new StringBuilder(128);// 遍歷字節(jié)數(shù)組for (int i = 0; i < charArray.length; i++) {// 檢測當前字符是否為大寫字母if (Character.isUpperCase(charArray[i])) {if (i != 0) {// 向 sb 中添加點號sb.append(".");}// 將字符變?yōu)樾?#xff0c;并添加到 sb 中sb.append(Character.toLowerCase(charArray[i]));} else {// 添加字符到 sb 中sb.append(charArray[i]);}}value = new String[]{sb.toString()};}// 省略無關代碼}// 省略無關邏輯 } |
?2.2.3.4 檢測 Invocation 參數(shù)
此段邏輯是檢測方法列表中是否存在 Invocation 類型的參數(shù),若存在,則為其生成判空代碼和其他一些代碼。相應的邏輯如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | for (Method method : methods) {Class<?> rt = method.getReturnType();Class<?>[] pts = method.getParameterTypes(); // 獲取參數(shù)類型列表Class<?>[] ets = method.getExceptionTypes();Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);StringBuilder code = new StringBuilder(512);if (adaptiveAnnotation == null) {// ${無 Adaptive 注解方法代碼生成}} else {// ${獲取 URL 數(shù)據(jù)}// ${獲取 Adaptive 注解值}boolean hasInvocation = false;// 遍歷參數(shù)類型列表for (int i = 0; i < pts.length; ++i) {// 判斷當前參數(shù)名稱是否等于 com.alibaba.dubbo.rpc.Invocationif (pts[i].getName().equals("com.alibaba.dubbo.rpc.Invocation")) {// 為 Invocation 類型參數(shù)生成判空代碼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 為 truehasInvocation = true;break;}}}// 省略無關邏輯 } |
?2.2.3.5 生成拓展名獲取邏輯
本段邏輯用于根據(jù) SPI 和 Adaptive 注解值生成“拓展名獲取邏輯”,同時生成邏輯也受 Invocation 類型參數(shù)影響,綜合因素導致本段邏輯相對復雜。本段邏輯可以會生成但不限于下面的代碼:
| 1 | String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol()); |
或
| 1 | String extName = url.getMethodParameter(methodName, "loadbalance", "random"); |
亦或是
| 1 | String extName = url.getParameter("client", url.getParameter("transporter", "netty")); |
本段邏輯復雜指出在于條件分支比較多,大家在閱讀源碼時需要知道每個條件分支的意義是什么,否則不太容易看懂相關代碼。好了,其他的就不多說了,開始分析本段邏輯。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | 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 數(shù)據(jù)}// ${獲取 Adaptive 注解值}// ${檢測 Invocation 參數(shù)}// 設置默認拓展名,cachedDefaultName = SPI 注解值,比如 Protocol 接口上標注的 // SPI 注解值為 dubbo。默認情況下,SPI 注解值為空串,此時 cachedDefaultName = nullString defaultExtName = cachedDefaultName;String getNameCode = null;// 遍歷 value,這里的 value 是 Adaptive 的注解值,2.2.3.3 節(jié)分析過 value 變量的獲取過程。// 此處循環(huán)目的是生成從 URL 中獲取拓展名的代碼,生成的代碼會賦值給 getNameCode 變量。注意這// 個循環(huán)的遍歷順序是由后向前遍歷的。for (int i = value.length - 1; i >= 0; --i) {if (i == value.length - 1) { // 當 i 為最后一個元素的坐標時if (null != defaultExtName) { // 默認拓展名非空// protocol 是 url 的一部分,可通過 getProtocol 方法獲取,其他的則是從// URL 參數(shù)中獲取。所以這里要判斷 value[i] 是否為 protocolif (!"protocol".equals(value[i]))// hasInvocation 用于標識方法參數(shù)列表中是否有 Invocation 類型參數(shù)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 中獲取協(xié)議的代碼,比如 "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);}// 省略無關邏輯 } |
上面代碼已經(jīng)進行了大量的注釋,不過看起來任然不是很好理解。既然如此,那么建議大家寫點測試代碼,對 Protocol、LoadBalance 以及 Transporter 等接口的自適應拓展類代碼生成過程進行調試。這里我以 Transporter 接口的自適應拓展類代碼生成過程進行分析。首先看一下 Transporter 接口的定義,如下:
| 1 2 3 4 5 6 7 8 9 10 | @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 方法代理邏輯生成的過程進行分析,此時生成代理邏輯所用到的變量和值如下:
| 1 2 3 4 | String defaultExtName = "netty"; boolean hasInvocation = false; String getNameCode = null; String[] value = ["client", "transporter"]; |
下面對 value 數(shù)組進行遍歷,此時 i = 1, value[i] = “transporter”,生成的代碼如下:
| 1 | getNameCode = url.getParameter("transporter", "netty"); |
接下來,for 循環(huán)繼續(xù)執(zhí)行,此時 i = 0, value[i] = “client”,生成的代碼如下:
| 1 | getNameCode = url.getParameter("client", url.getParameter("transporter", "netty")); |
for 循環(huán)結束運行,現(xiàn)在生成 extName 變量及判空代碼,如下:
| 1 2 3 4 5 6 | 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 方法的拓展名獲取代碼就生成好了。如果大家不是很明白,建議自己調試走一遍。好了,本節(jié)先到這里。
?2.2.3.6 生成拓展加載與目標方法調用邏輯
上一節(jié)的邏輯生成拓展名 extName 獲取邏輯,接下來要做的是根據(jù)拓展名加載拓展實例,并調用拓展實例的目標方法。相關邏輯如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | 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 數(shù)據(jù)}// ${獲取 Adaptive 注解值}// ${檢測 Invocation 參數(shù)}// ${生成拓展名獲取邏輯}// 生成拓展獲取代碼,格式如下:// type全限定名 extension = (type全限定名)ExtensionLoader全限定名// .getExtensionLoader(type全限定名.class).getExtension(extName);// Tips: 格式化字符串中的 %<s 表示使用前一個轉換符所描述的參數(shù),即 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 接口舉例說明,上面代碼生成的內(nèi)容如下:
| 1 2 3 | 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 生成完整的方法
本節(jié)進行代碼生成的收尾工作,主要用于生成方法定義的代碼。相關邏輯如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | 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 數(shù)據(jù)}// ${獲取 Adaptive 注解值}// ${檢測 Invocation 參數(shù)}// ${生成拓展名獲取邏輯}// ${生成拓展加載與目標方法調用邏輯}} }// public + 返回值全限定名 + 方法名 + ( codeBuilder.append("\npublic ").append(rt.getCanonicalName()).append(" ").append(method.getName()).append("(");// 添加參數(shù)列表代碼 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 方法為例,上面代碼生成的內(nèi)容如下:
| 1 2 3 | public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) {// 方法體 } |
?3.基于動態(tài)代理實現(xiàn)知識與拓展
我在第一章介紹自適應拓展原理時說過,Dubbo 通過生成和編譯代碼實現(xiàn)自適應拓展的方式有點復雜,不利于維護。另外,這樣做對源碼學習讀者來說,也不是很友好。我敢肯定,有同學會像我一樣,在開始調試 Dubbo 源碼時,不知道如何調試各種自適應拓展類,比如 Protocol$Adaptive。如果你也有類似的困惑,這里教大家一個方法。如下:
以 Protocol 接口為例,當代碼越過斷點后,調試信息如下:
從調試信息中可知,ProtocolAdaptive 所在包為 com.alibaba.dubbo.rpc。因此接下來到 com.alibaba.dubbo.rpc 包下創(chuàng)建 ProtocolAdaptive 類,并把 code 變量值拷貝到剛創(chuàng)建的文件中。當我們再次進行調試時,就能進入內(nèi)部了。比如:
既然 Dubbo 實現(xiàn)的 Adaptive 機制不利于調試,那么我們可以對其進行改造。改造后的代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | 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);}@Overridepublic 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 數(shù)據(jù)URL url = getUrlData(method, args);// 獲取 Adaptive 注解值String[] value = getAdaptiveAnnotationValue(method);// 獲取 Invocation 參數(shù)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 數(shù)據(jù)以及 Adaptive 注解值的獲取邏輯封裝在了私有方法中,相應的代碼如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | 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; } |
現(xiàn)在我們將 AdaptiveInvokeHandler 放置到 ExtensionLoader 所在包下,并對 ExtensionLoader 的 createAdaptiveExtension 方法代碼進行改造。如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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.總結
到此,關于自適應拓展的原理,實現(xiàn)以及改造過程就分析完了。總的來說自適應拓展整個邏輯還是很復雜的,并不是很容易弄懂。因此,大家在閱讀該部分源碼時,耐心一些,同時多進行調試。亦或是通過生成好的代碼思考生成邏輯。當然,大家也可以將代碼生成邏輯看成一個黑盒,不懂細節(jié)也沒關系,只要知道自適應拓展原理即可。
好了,本篇文章先到這里,感謝大家的閱讀。
- 本文鏈接:?https://www.tianxiaobo.com/2018/10/13/Dubbo-源碼分析-自適應拓展原理/
http://www.tianxiaobo.com/2018/10/13/Dubbo-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-%E8%87%AA%E9%80%82%E5%BA%94%E6%8B%93%E5%B1%95%E5%8E%9F%E7%90%86/?
總結
以上是生活随笔為你收集整理的Dubbo 源码分析 - 自适应拓展原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Dubbo 源码分析 - SPI 机制
- 下一篇: Dubbo 源码分析 - 服务导出