Dubbo 重點,SPI 的自適應擴展原理
本文從爲什麼需要自適應擴展的提問引出自己如何實現以及推理 Dubbo 如何實現,這些鋪墊可以幫助讀者更好的理解後文對 Dubbo 自適應擴展源碼的解讀。
很多人在學習 SPI 的時候將@SPI
和@Adaptive
註解混在一起學習,最後學得暈暈乎乎看完之後似懂非懂,如果你也有這種困擾,請繼續閱讀。
並不是說不該將這兩個內容一起學習,而是要有個先後順序再加上自己的推理。是先有 SPI 機制,然後纔有的自適應擴展,自適應擴展是基於 SPI 機制的高級特性。如果你還不懂 Dubbo SPI 原理,請移步這篇文章。
爲什麼需要自適應擴展點
在 Dubbo 中,很多拓展都是通過 SPI 機制進行加載的,比如 Protocol、Cluster、LoadBalance 等。有時,有些拓展並不想在框架啓動階段被加載,而是希望在拓展方法被調用時,根據運行時參數進行加載。這聽起來有些矛盾。拓展未被加載,那麼拓展方法就無法被調用(靜態方法除外)。拓展方法未被調用,拓展就無法被加載。對於這個矛盾的問題,Dubbo 通過自適應拓展機制很好的解決了。
對於這個問題,以之前 demo 爲例進行我們進行推演:
/**
* @author 後端開發技術
*/
public interface MySPI {
void say();
}
public class HelloMySPI implements MySPI{
@Override
public void say() {
System.out.println("HelloMySPI say:hello");
}
}
public class GoodbyeMySPI implements MySPI {
@Override
public void say() {
System.out.println("GoodbyeMySPI say:Goodbye");
}
}
現在要增加一個接口 Person,他可以和人打招呼。他有一個實現類是Man
,他可以動態的跟人說 hello 或者 goodbye。
public interface Person {
void greeting();
}
public class Man implements Person{
private MySPI adaptive;
public void setAdaptive(MySPI adaptive) {
this.adaptive = adaptive;
}
@Override
public void greeting(URL url) {
adaptive.say(url);
}
}
但是adaptive
成員要麼是HelloMySPI
的實例化對象,要麼是GoodbyeMySPI
的實例化對象,怎麼實現動態的去根據需要獲取呢?解決這個問題就可以增加一個代理,作爲自適應類。所以增加自適應擴展實現如下:
public class AdaptiveMySPI implements MySPI {
@Override
public void say() {
// 1.通過某種方式獲得拓展實現的名稱
String mySpiName;
// 2.通過 SPI 加載具體的 mySpi
MySPI myspi = ExtensionLoader.getExtensionLoader(MySPI.class).getExtension(mySpiName);
// 3.調用目標方法
myspi.say();
}
}
將代理類AdaptiveMySPI
作爲Man
的成員對象,這樣就可以實現按需調用了。按需加載如何實現呢?之前我們在getExtension()
方法中提到過,只要在根據名字查找的時候,纔會按照需要懶加載,所以這個問題天然被 Dubbo SPI 解決了。那麼剩下的關鍵就是如何按需調用,也就是如何獲得名字。
-
可以在當前線程的上下文中獲得,比如通過 ThreadLocal 保存。
-
可以通過接口參數傳遞,但是這樣就需要實現自適應擴展的接口按照約定去定義參數,否則就無法拿到名字,這樣對於被代理的接口是有一定限制的。
Dubbo 用的是第二種方式,也就是他總有辦法從參數中動態拿到擴展類名。
模擬原理 demo
再具體一些 Dubbo 是怎麼實現的呢?
自適應拓展機制的實現邏輯比較複雜,首先 Dubbo 會爲拓展接口生成具有代理功能的代碼。然後通過 javassist 或 jdk 編譯這段代碼,得到 Class 類。最後再通過反射創建代理類,整個過程比較複雜。爲了讓大家對自適應拓展有一個感性的認識,按照之前的知識,下面我們繼續對之前 demo 爲例進行改造:
@SPI
public interface MySPI {
void say(URL url);
}
public class HelloMySPI implements MySPI{
@Override
public void say(URL url) {
System.out.println("HelloMySPI say:hello");
}
}
public class GoodbyeMySPI implements MySPI {
@Override
public void say(URL url) {
System.out.println("GoodbyeMySPI say:Goodbye");
}
}
public class AdaptiveMySPI implements MySPI {
@Override
public void say(URL url) {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
// 1.從 URL 中獲取 mySpi 名稱
String mySpiName = url.getParameter("myspi.type");
if (mySpiName == null) {
throw new IllegalArgumentException("MySPI == null");
}
// 2.通過 SPI 加載具體的 mySpi
MySPI myspi = ExtensionLoader.getExtensionLoader(MySPI.class).getExtension(mySpiName);
// 3.調用目標方法
myspi.say(url);
}
}
@SPI("man")
public interface Person {
void greeting(URL url);
}
public class Man implements Person {
private MySPI adaptive = = ExtensionLoader.getExtensionLoader(MySPI.class).getExtension(“adaptive”);
public void setAdaptive(MySPI adaptive) {
this.adaptive = adaptive;
}
@Override
public void greeting(URL url) {
adaptive.say(url);
}
}
public static void main(String[] args) {
ExtensionLoader<Person> extensionLoader = ExtensionLoader.getExtensionLoader(Person.class);
Person hello = extensionLoader.getExtension("man");
hello.greeting(URL.valueOf("dubbo://192.168.0.101:100?myspi.type=hello"));
hello.greeting(URL.valueOf("dubbo://192.168.0.101:100?myspi.type=goodbye"));
}
//輸出
HelloMySPI say:hello
GoodbyeMySPI say:Goodbye
大家與之前的代碼對比就可以發現區別,MySPI 的方法增加了 URL 參數,因爲 dubbo 中 url 就是作爲一個配置總線貫穿整個調用鏈路的。這樣便可以拿到擴展名,動態調用和加載了。
比如 demo 中的一個 URL
dubbo://192.168.0.101:100?myspi.type=hello
AdaptiveMySPI 動態的人從 url 中拿到了myspi.type=hello
,然後根據 name 拿到了擴展實現,以此完成動態調用。
上面的示例展示了自適應拓展類的核心實現 —- 在拓展接口的方法被調用時,通過 SPI 加載具體的拓展實現類,並調用拓展對象的同名方法。所以接下來的關鍵就在於,自適應拓展類是如何生成的,Dubbo 是怎麼做的。
@Adaptive 註解
關於 Dubbo 的自適應擴展,一定避不開這個關鍵註解@Adaptive
。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
String[] value() default {};
}
從上面的代碼中可知,Adaptive 可註解在類或方法上。
-
當 Adaptive 註解在類上時,Dubbo 不會爲該類生成代理類。Adaptive 註解在類上的情況很少,在 Dubbo 中,僅有兩個類被 Adaptive 註解了,分別是 AdaptiveCompiler 和 AdaptiveExtensionFactory。此種情況,表示拓展的加載邏輯由人工編碼完成。
-
註解在方法(接口方法)上時,Dubbo 則會爲該方法生成代理邏輯。更多時候,Adaptive 是註解在接口方法上的,表示拓展的加載邏輯需由框架自動生成。
Adaptive 註解的地方不同,相應的處理邏輯也是不同的。註解在類上時,處理邏輯比較簡單,本文就不分析了。註解在接口方法上時,處理邏輯較爲複雜,本章將會重點分析此塊邏輯。
你有沒有想過爲什麼可以設計加載在類上?
按照我們上述的推理,實現自適應擴展總需要按照一定的約束和規範去設計方法參數,這樣才能拿到參數去動態的加載擴展類。如果就是沒有按照規範去設計參數呢?那就需要自己去想辦法實現了,無法生成代理類,這就是加載在類上的原因。
源碼解讀
獲得自適應類
在 Dubbo 中如何獲得一個自適應擴展類呢?只需要一行代碼。
MySPI adaptive = ExtensionLoader.getExtensionLoader(MySPI.class).getAdaptiveExtension();
@SPI
public interface MySPI {
@Adaptive(value = {"myspi.type"})
void say(URL url);
}
getExtensionLoader()
之前我們依舊分析過了,這裏直接從getAdaptiveExtension()
開始。
getAdaptiveExtension()
方法是獲取自適應拓展的入口方法,首先會檢查緩存cachedAdaptiveInstance
,緩存未命中,則會執行雙重檢查,調用 createAdaptiveExtension()
方法創建自適應拓展。
public T getAdaptiveExtension() {
// 從緩存中查找自適應擴展類實例,命中直接返回
Object instance = cachedAdaptiveInstance.get();
//緩存沒有命中
if (instance == null) {
…… 異常處理
//雙重檢查
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
// 創建自適應實例
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
…… 異常處理
return (T) instance;
}
createAdaptiveExtension()
會首先查找只適應擴展類,然後通過反射進行實例化,再調用injectExtension
對擴展實例中注入依賴。
你可能會問直接返回依賴不就行了?爲什麼還需要注入?
這是因爲任何利用 Dubbo SPI 機制加載的用戶創建類都是有可能有成員依賴於其他拓展類的,用戶實現的自適應擴展類也不例外。而另一種 Dubbo 自己生成的自適應擴展類則不可能出現依賴其他類的情況。
這裏只關注重點方法getAdaptiveExtensionClass()
。
-
首先
getExtensionClasses()
會獲取該接口所有的拓展類, -
然後會檢查緩存是否爲空,
cachedAdaptiveClass
緩存着自適應擴展類的類型。 -
如果緩存中不存在則調用
createAdaptiveExtensionClass
開始創建自適應擴展類。
// 用於緩存自適應擴展類的類型
private volatile Class<?> cachedAdaptiveClass = null;
private Class<?> getAdaptiveExtensionClass() {
// 加載所有的拓展類配置
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
// 創建自適應拓展類
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
getExtensionClasses
之前講過,你肯定會奇怪爲什麼還需要加載所有擴展類。在這裏有個關鍵邏輯,在調用 loadResource 方法時候會解析 @Adaptive 註解,如果被標註了,就表示這個類是一個自適應擴展類實現,會被設置到緩存cacheAdaptiveClass
中。
所以有兩個原因:
1、是自定義自適應擴展類需要 SPI 機制加載
2、是設置緩存
生成自適應擴展類
非自己實現的自適應擴展類,都要走createAdaptiveExtensionClass
邏輯。
主要邏輯如下:
-
動態生成自適應擴展類代碼
-
獲取類加載器和編譯器類(Dubbo 默認使用 javassist 作爲編譯器)
-
編譯、加載動態生成的類
private Class<?> createAdaptiveExtensionClass() {
// 動態生成自適應擴展類代碼
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
// 獲取類加載器
ClassLoader classLoader = findClassLoader();
// 獲取編譯器類 ⚠️ AdaptiveCompiler 也是自己定義的
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
//編譯、加載、生產Class
return compiler.compile(code, classLoader);
}
生成自適應擴展類的代碼都在AdaptiveClassCodeGenerator
中,generate()
方法會生成和返回一個自適應擴展類。之前的版本代碼其實比較複雜,邏輯都寫在了一起,並沒有 generate
方法,進入 Apache 孵化之後對代碼結構進行了調整,結構清晰了許多。
主要邏輯如下:
-
檢查接口是否有方法被 @Adaptive 修飾。
-
生產 class 頭部的 package 信息。
-
生成依賴類的 import 信息。
-
生成方法聲明信息。
-
遍歷接口方法依次生成實現方法。
-
類結束用
}
收尾,類信息轉換爲字符串返回。
public String generate() {
// no need to generate adaptive class since there's no adaptive method found.
// 檢查接口是否有註解了Adaptive的方法,至少需要有一個
if (!hasAdaptiveMethod()) {
throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
}
StringBuilder code = new StringBuilder();
// 生成package信息
code.append(generatePackageInfo());
// 生成依賴類的import信息
code.append(generateImports());
// 生成類的聲明信息 public class 接口名$Adaptive implements 接口名
code.append(generateClassDeclaration());
// 遍歷接口方法 按需生產實現類
Method[] methods = type.getMethods();
for (Method method : methods) {
code.append(generateMethod(method));
}
// 類結尾
code.append("}");
if (logger.isDebugEnabled()) {
logger.debug(code.toString());
}
// 轉換爲字符串返回
return code.toString();
}
下面依次介紹上述步驟的主要邏輯。
檢查 @Adaptive 註解
遍歷接口方法依次檢查是否被 @Adaptive 標註,至少需要有一個方法被註解,否則拋出異常。
private boolean hasAdaptiveMethod() {
return Arrays.stream(type.getMethods()).anyMatch(m -> m.isAnnotationPresent(Adaptive.class));
}
生成 package 和 import 信息
按照接口的路徑名生成對應的 package 信息,並且生成導入信息,目前只是 import 了ExtensionLoader
這個類。
private static final String CODE_PACKAGE = "package %s;\n";
private String generatePackageInfo() {
return String.format(CODE_PACKAGE, type.getPackage().getName());
}
private static final String CODE_IMPORTS = "import %s;\n";
private String generateImports() {
return String.format(CODE_IMPORTS, ExtensionLoader.class.getName());
}
// import org.apache.dubbo.common.extension.ExtensionLoader;
生成類的聲明信息
生成的類名 = 拓展接口名 +$Adaptive,實現的接口就是拓展接口的全限定名。比如 public class MySPI$Adaptive implements org.daley.spi.demo.MySPI
private String generateClassDeclaration() {
return String.format(CODE_CLASS_DECLARATION, type.getSimpleName(), type.getCanonicalName());
}
private static final String CODE_CLASS_DECLARATION = "public class %s$Adaptive implements %s {\n";
生成方法體
generateMethod
方法是自適應拓展類生成代理類的核心邏輯所在。它主要會分別拿到方法返回類型、方法名、生成方法體、生成方法參數、生成方法異常,然後按照方法的模板的佔位符生成代理方法。很明顯,重中之重是生成方法體內容。
private String generateMethod(Method method) {
// 分別拿到方法返回類型、方法名、方法體、方法參數、方法異常
String methodReturnType = method.getReturnType().getCanonicalName();
String methodName = method.getName();
// 生成方法體
String methodContent = generateMethodContent(method);
String methodArgs = generateMethodArguments(method);
String methodThrows = generateMethodThrows(method);
// 按照方法模板替換佔位符,生成方法內容
return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
}
generateMethodContent
主要做了如下幾件事:
-
檢查是否被 @Adaptive 註解,如果沒有被註解則生產一段拋出異常的代碼。如果被註解,則繼續後面邏輯。
-
找到 URL 類型參數的 index,並且生成檢查 URL 參數是否爲空的邏輯。
-
如果沒有 URL 參數,則檢查是否方法參數有 public 類型無參 get 方法可以直接拿到 URL。
-
拿到 @Adaptive 註解配置的 value,如果沒有配置就用接口名默認。
-
檢查是否有 Invocation 類型參數。
-
根據不同的情況拿到拓展名。
-
根據擴展名從
getExtension
中拿到真正的擴展類。 -
執行擴展類目標方法,按需返回結果。
這些步驟邏輯都不算複雜,需要格外注意的是第 6 點,這裏詳細再說明下。
private String generateMethodContent(Method method) {
Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
StringBuilder code = new StringBuilder(512);
// 檢查是否被@Adaptive註解
if (adaptiveAnnotation == null) {
// 不是自適應方法生成的代碼是一段拋出異常的代碼。
return generateUnsupported(method);
} else {
// 找到 URL.class 類型參數位置
int urlTypeIndex = getUrlTypeIndex(method);
// found parameter in URL type
if (urlTypeIndex != -1) {
// Null Point check
// 找到 URL 生成代碼邏輯
code.append(generateUrlNullCheck(urlTypeIndex));
} else {
// did not find parameter in URL type
// 未找到 URL 生成代碼邏輯
// 再找找是否有方法參數有get方法可以返回URL.class的,並且還是不需要入參的public方法
code.append(generateUrlAssignmentIndirectly(method));
}
// 拿到 Adaptive註解的value
String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
// 有 Invocation 類型參數
boolean hasInvocation = hasInvocationArgument(method);
// 檢查參數不爲null
code.append(generateInvocationArgumentNullCheck(method));
code.append(generateExtNameAssignment(value, hasInvocation));
// check extName == null?
code.append(generateExtNameNullCheck(value));
// getExtension 根據name拿到擴展類
code.append(generateExtensionAssignment());
// return statement 生成方法調用語句並在必要時返回
code.append(generateReturnAndInvocation(method));
}
return code.toString();
}
在generateExtNameAssignment
中會有如下幾種不同的情況:是否最後一個參數,是否有 Invocation 類型參數、是否配置了名字爲protocol
的註解 value。
將上面的三種條件組合,生成對應不同的代碼,核心其實都是如何正確的從 URL 參數中拿到動態擴展名,具體已做註釋。
private String generateExtNameAssignment(String[] value, boolean hasInvocation) {
// TODO: refactor it
String getNameCode = null;
// 從最後一個開始遍歷
for (int i = value.length - 1; i >= 0; --i) {
if (i == value.length - 1) {
// 如果是最後一個參數,設置了默認拓展名
if (null != defaultExtName) {
// 配置的value不等於"protocol"
if (!"protocol".equals(value[i])) {
if (hasInvocation) {
// 有invocation 根據配置名字從url獲取getMethodParameter
getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
} else {
// 沒有invocation,getParameter獲取參數
getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
}
} else {
// 直接取協議名
getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )", defaultExtName);
}
} else {
//沒有設置默認拓展名,和上面的區別就是 沒有默認值處理的邏輯。上面獲取不到可以直接用默認值。
if (!"protocol".equals(value[i])) {
if (hasInvocation) {
getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
} else {
getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
}
} else {
getNameCode = "url.getProtocol()";
}
}
} else {
// 如果不是最後一個參數
if (!"protocol".equals(value[i])) {
if (hasInvocation) {
getNameCode = String.format("url.getMethodParameter(methodName, \"%s\", \"%s\")", value[i], defaultExtName);
} else {
getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
}
} else {
getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
}
}
}
return String.format(CODE_EXT_NAME_ASSIGNMENT, getNameCode);
}
對於上述條件,可以生成如下幾種情況的代碼。
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
String extName = url.getMethodParameter(methodName, "loadbalance", "random");
String extName = url.getParameter("client", url.getParameter("transporter", "netty"));
還有一個疑問點是如果有多個參數怎麼辦?按照代碼邏輯,最終的表現效果就是如果有多個參數,且非 Invocation,會生成多層嵌套代碼,並且以最外層也就是最左邊的參數爲準,右邊的參數作爲默認值。
舉個例子:
@Adaptive(value = {"protocol","param2","myspi.type"})
void say(URL url);
生成代碼如下:
url.getProtocol() == null ? (url.getParameter("param2", url.getParameter("myspi.type"))) : url.getProtocol()
當上述一切代碼執行完成後,就生成了最終的代理類,並且經過編譯和加載最終完成實例化,可以被程序所調用,實現動態按需調用。
最終生成的代理類如下:
package org.daley.spi.demo;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class MySPI$Adaptive implements org.daley.spi.demo.MySPI {
public void say(org.apache.dubbo.common.URL arg0) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
String extName = url.getProtocol() == null ?(url.getParameter("param2",url.getParameter("myspi.type"))) : url.getProtocol();
if(extName == null) throw new IllegalStateException("Failed to get extension (org.daley.spi.demo.MySPI) name from url (" + url.toString() + ") use keys([protocol, param2, myspi.type])");
org.daley.spi.demo.MySPI extension = (org.daley.spi.demo.MySPI)ExtensionLoader.getExtensionLoader(org.daley.spi.demo.MySPI.class).getExtension(extName);
extension.say(arg0);
}
}
到這裏,Dubbo 自適應擴展的原理就講解結束了。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/AIMsttV0sk--bG22k69zlA