開篇
本文嘗試通過一個示例來講解Adaptive機制的用法,然後會從源碼的角度對其實作原理進行講解
Adaptive機制
對應于Adaptive機制,Dubbo提供了一個注解@Adaptive,該注解可以用于接口的某個實作類上,也可以用于接口方法上。如果用在接口的子類上,則表示Adaptive機制的實作會按照該子類的方式進行自定義實作;如果用在方法上,則表示Dubbo會為該接口自動生成一個子類,并且按照一定的格式重寫該方法,而其餘沒有标注@Adaptive注解的方法将會預設抛出異常。
對于第一種Adaptive的使用方式,Dubbo裡隻有ExtensionFactory接口使用了,其有一個子類AdaptiveExtensionFactory就使用了@Adaptive注解進行了标注,主要作用就是在擷取目标對象時,分别通過ExtensionLoader和Spring容器兩種方式擷取,主要用在SPI的IOC實作中用于擷取上下文的Bean對象。
對于第二種使用@Adaptive注解标注在接口方法上以實作Adaptive機制的使用原理,通過一個下面的例子進行說明。
用法示例
例子說明
- 定義PlantsWater的接口并通過@SPI注解進行注解,注解可選擇帶預設值。
- 将watering()方法使用@Adaptive注解進行了标注,表示該方法在自動生成的子類中是需要動态實作的方法。
- 增加grant()方法是為了表明不帶@Adaptive在自動生成的子類方法内部會抛出異常。
- 為PlantsWater增加兩個實作,AppleWater和BananaWater,實際調用通過參數控制。
- 在META-INF/dubbo下建立一個檔案,該檔案的名稱是目标接口的全限定名,這裡是org.apache.dubbo.spi.example.PlantsWater,在該檔案中需要指定該接口所有可提供服務的子類。
- 定義主函數ExtensionLoaderDemo模拟SPI調用的驗證。
----定義基礎應用類
public interface Fruit {}
public class Apple implements Fruit {}
public class Banana implements Fruit{}
----定義SPI類
@SPI("banana")
public interface PlantsWater {
Fruit grant();
@Adaptive
String watering(URL url);
}
public class AppleWater implements PlantsWater {
public Fruit grant() {
return new Apple();
}
public String watering(URL url) {
System.out.println("watering apple");
return "watering finished";
}
}
public class BananaWater implements PlantsWater {
public Fruit grant() {
return new Banana();
}
public String watering(URL url) {
System.out.println("watering banana");
return "watering success";
}
}
----resources檔案 org.apache.dubbo.spi.example.PlantsWater
apple=org.apache.dubbo.spi.example.AppleWater
banana=org.apache.dubbo.spi.example.BananaWater
------測試代碼内容
public class ExtensionLoaderDemo {
public static void main(String[] args) {
// 首先建立一個模拟用的URL對象
URL url = URL.valueOf("dubbo://192.168.0.101:20880?plants.water=apple");
// 通過ExtensionLoader擷取一個PlantsWater對象,getAdaptiveExtension已經加載了所有SPI類
PlantsWater plantsWater = ExtensionLoader.getExtensionLoader(PlantsWater.class)
.getAdaptiveExtension();
// 使用該PlantsWater調用其"自适應标注的"方法,擷取調用結果
String result = plantsWater.watering(url);
System.out.println(result);
}
}
-----實際輸出内容
十月 11, 2019 7:48:51 下午 org.apache.dubbo.common.logger.LoggerFactory info
資訊: using logger: org.apache.dubbo.common.logger.jcl.JclLoggerAdapter
watering apple
watering finished
Process finished with exit code 0
原理補充
- 這裡提供了AppleWater和BananaWater表示的其實是兩種基礎服務類,本質上它們三者的關系是PlantsWater用于對外提供一個規範,而AppleWater和BananaWater則是實作了這種規範的兩種基礎服務。至于調用方需要使用哪種基礎服務來實作其功能,這就需要根據調用方指定的參數來動态選取的,而@Adaptive機制就是提供了這樣一種選取功能。
- 模拟調用構造了一個URL對象,這個URL對象是Dubbo中進行參數傳遞所使用的一個基礎類,在配置檔案中配置的屬性都會被封裝到該對象中。這裡我們主要要注意該對象是通過一個url構造的,并且url的最後我們有一個參數plants.water=apple,這裡其實就是我們所指定的使用哪種基礎服務類的參數。比如這裡指定的就是使用apple對應的AppleWater。
- ExtensionLoader.getExtensionLoader(PlantsWater.class).getAdaptiveExtension()的過程中會生成PlantsWater類對應的ExtensionLoader,在該Loader裡面儲存了PlantsWater對應的SPI的各個具體實作類。
PlantsWater$Adaptive
說明:
- PlantsWater$Adaptive是PlantsWater接口動态生成的子類,通過ExtensionLoader.getAdaptiveExtension()方法動态生成。
- 針對沒有@Adaptive修飾的方法,子類中實作的方法體内部直接抛出UnsupportedOperationException。
- 針對有@Adaptive修飾的方法,子類會重新實作動态擷取PlantsWater 執行個體的方法,ExtensionLoader.getExtensionLoader(PlantsWater.class).getExtension(extName)。
- 在使用@Adaptive注解标注的方法中,其參數中必須有一個參數類型為URL,或者其某個參數提供了某個方法,該方法可以傳回一個URL對象。
- 在方法的實作中會通過URL對象擷取某個參數對應的參數值,如果在接口的@SPI注解中指定了預設值,那麼在使用URL對象擷取參數值時,如果沒有取到,就會使用該預設值。url.getParameter("plants.water", "banana")中plants.water取URL中帶的參數,"banana"屬于SPI指定的預設值。
- 根據擷取到的參數值,在ExtensionLoader中擷取該參數值對應的服務提供類對象(通過ExtensionLoader.getExtensionLoader().getExtension(extName)),然後将真正的調用委托給該服務提供類對象進行。
- 在通過URL對象擷取參數時,參數key擷取的對應規則是,首先會從@Adaptive注解的參數值中擷取,如果該注解沒有指定參數名,那麼就會預設将目标接口的類名轉換為點分形式作為參數名,比如這裡PlantsWater轉換為點分形式就是plants.water。
package org.apache.dubbo.spi.example;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class PlantsWater$Adaptive implements org.apache.dubbo.spi.example.PlantsWater {
public java.lang.String watering(org.apache.dubbo.common.URL arg0) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
String extName = url.getParameter("plants.water", "banana");
if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.spi.example.PlantsWater) name from url (" + url.toString() + ") use keys([plants.water])");
org.apache.dubbo.spi.example.PlantsWater extension = (org.apache.dubbo.spi.example.PlantsWater)ExtensionLoader
.getExtensionLoader(org.apache.dubbo.spi.example.PlantsWater.class)
.getExtension(extName);
return extension.watering(arg0);
}
public org.apache.dubbo.spi.example.Fruit grant() {
throw new UnsupportedOperationException("The method public abstract org.apache.dubbo.spi.example.Fruit org.apache.dubbo.spi.example.PlantsWater.grant() of interface org.apache.dubbo.spi.example.PlantsWater is not adaptive method!");
}
}
實作原理
Dubbo Adaptive的實作機制根據上面的講解其實步驟已經比較清晰了,主要分為如下三個步驟:
- 1、加載标注有@Adaptive注解的接口,如果不存在,則不支援Adaptive機制;
- 2、為目标接口按照一定的模闆生成子類代碼,并且編譯生成的代碼,然後通過反射生成該類的對象;
- 3、結合生成的對象執行個體,通過傳入的URL對象,擷取指定key的配置,然後加載該key對應的類對象,最終将調用委托給該類對象進行。
可以看到,通過這種方式,Dubbo就實作了一種通過配置參數動态選擇所使用的服務的目的,而實作這種機制的入口主要在ExtensionLoader.getAdaptiveExtension()方法
public class ExtensionLoaderDemo {
public static void main(String[] args) {
// 首先建立一個模拟用的URL對象
URL url = URL.valueOf("dubbo://192.168.0.101:20880?plants.water=apple");
// 通過ExtensionLoader擷取一個FruitGranter對象
PlantsWater plantsWater = ExtensionLoader.getExtensionLoader(PlantsWater.class)
.getAdaptiveExtension();
// 使用該FruitGranter調用其"自适應标注的"方法,擷取調用結果
String result = plantsWater.watering(url);
System.out.println(result);
}
}
-
上面的代碼作為整個動态生成代碼的入口,關注ExtensionLoader.getExtensionLoader(PlantsWater.class)
.getAdaptiveExtension()就可以了。
public T getAdaptiveExtension() {
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
if (createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
// 建立Adaptive執行個體
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("Failed to create adaptive "
+ "instance: " + t.toString(), t);
}
}
}
} else {
throw new IllegalStateException("Failed to create adaptive instance: "
+ createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
}
return (T) instance;
}
上面的代碼首先通過雙檢查法來從緩存中擷取Adaptive執行個體,如果沒擷取到,則建立一個。我們這裡繼續看createAdaptiveExtension()方法的實作。
private T createAdaptiveExtension() {
try {
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
}
}
上面代碼是建立Adaptive執行個體的方法是一個主幹方法,從這裡調用方法的順序就可以看出其主要作用:
- 擷取一個Adaptive類的class對象,不存在則建立一個,該方法會保證一定存在一個該class對象;
- 通過反射建立一個Adaptive類的執行個體;
- 對建立的Adaptive注入相關屬性,需要注意的是,Dubbo目前隻支援通過setter方法注入屬性。
private Class<?> getAdaptiveExtensionClass() {
// 通過讀取Dubbo的配置檔案,擷取其中的SPI類,其主要處理了四部分的類:
// 1. 标注了@Activate注解的類,該注解的主要作用是将某個實作子類标注為自動激活,也就是在加載
// 執行個體的時候也會加載該類的對象;
// 2. 記錄目标接口是否标注了@Adaptive注解,如果标注了該注解,則表示需要為該接口動态生成子類,或者說
// 目标接口是否存在标注了@Adaptive注解的子類,如果存在,則直接使用該子類作為Adaptive類;
// 3. 檢查加載到的類是否包含有傳入目标接口參數的構造方法,如果是,則表示該類是一個代理類,也可以
// 将其了解為最終會被作為責任鍊進行調用的類,這些類最終會在目标類被調用的時候以類似于AOP的方式,
// 将目标類包裹起來,然後将包裹之後的類對外提供服務;
// 4. 剩餘的一般類就是實作了目标接口,并且作為基礎服務提供的類。
getExtensionClasses();
// 經過上面的類加載過程,如果目标接口某個子類存在@Adaptive注解,就會将其class對象緩存到
// cachedAdaptiveClass對象中。這裡我們就可以看到@Adaptive注解的兩種使用方式的分界點,也就是說,
// 如果某個子類标注了@Adaptive注解,那麼就會使用該子類所自定義的Adaptive機制,如果沒有子類标注了
// 該注解,那麼就會使用下面的createAdaptiveExtensionClass()方式來建立一個目标類class對象
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
// 建立一個目标接口的子類class對象
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
private Class<?> createAdaptiveExtensionClass() {
// 為目标接口生成子類代碼,以字元串形式表示
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
// 擷取classloader
ClassLoader classLoader = findClassLoader();
// 通過jdk或者javassist的方式編譯生成的子類字元串(預設是javassist),進而得到一個class對象
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}
上面的代碼中主要是一個骨架代碼,首先通過
getExtensionClasses()
擷取配置檔案中配置的各個類對象,其加載的原理可閱讀文章
Dubbo之SPI原理詳解;加載完成後,會通過
AdaptiveClassCodeGenerator
來為目标類生成子類代碼,并以字元串的形式傳回,最後通過javassist或jdk的方式進行編譯然後傳回class對象。這裡我們主要閱讀
AdaptiveClassCodeGenerator.generate()
方法是如何生成目标接口的子類的。
public String generate() {
//判斷目标接口是否有方法标注了@Adaptive注解,如果沒有則抛出異常
if (!hasAdaptiveMethod()) {
throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
}
StringBuilder code = new StringBuilder();
code.append(generatePackageInfo()); // 生成package資訊
code.append(generateImports()); // 生成import資訊,這裡隻導入了ExtensionLoader類,其餘的類都通過全限定名的方式來使用
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(); // 傳回生成的class代碼
}
上面代碼的generate()方法是生成目标類的主幹方法,其主要分為如下幾個步驟:
- 生成package資訊;
- 生成import資訊;
- 生成類聲明資訊;
-
生成各個方法的實作;
前面幾個步驟實作原理都相較比較簡單,繼續閱讀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); // 對方法進行格式化傳回
}
上面代碼的方法的生成,也拆分成了幾個子步驟,主要包括:
- 生成傳回值資訊;
- 生成方法名資訊;
- 生成方法參數資訊;
- 生成方法的異常資訊;
-
生成方法體資訊;
需要注意的是,這裡所使用的所有類都是使用的其全限定類名,通過前面我們展示的PlantsWater的子類代碼也可以看出這一點。上面生成的資訊中,方法的傳回值,方法名,方法參數以及異常資訊都可以通過接口聲明擷取到,而方法體則需要根據一定的邏輯來生成。關于方法參數,需要說明的是,Dubbo并沒有使用接口中對應參數的名稱,而是對每一個參數的參數名依次使用arg0、arg1等名稱。這裡我們繼續閱讀Dubbo生成方法體内容的代碼。
private String generateMethodContent(Method method) {
// 擷取方法上标注的@Adaptive注解,前面講到,Dubbo會使用該注解的值作為動态參數的key值
Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
StringBuilder code = new StringBuilder(512);
// 如果目前方法沒有标注@Adaptive注解,該方法的實作就會預設抛出異常
if (adaptiveAnnotation == null) {
return generateUnsupported(method);
} else {
// 擷取參數中類型為URL的參數所在的參數索引位置,因為我們的參數都是通過arg[i]的形式編排的,因而
// 擷取其索引就可以得到該參數的引用。這裡URL參數的主要作用是擷取目标參數對應的參數值
int urlTypeIndex = getUrlTypeIndex(method);
if (urlTypeIndex != -1) {
// 如果參數中存在URL類型的參數,那麼就為該參數進行空值檢查,如果為空,則抛出異常
code.append(generateUrlNullCheck(urlTypeIndex));
} else {
// 如果參數中不存在URL類型的參數,那麼就會檢查每個參數,判斷其是否有某個方法的傳回值是URL類型,
// 如果存在該方法,則首先對該參數進行空指針檢查,如果為空則抛出異常。然後調用該對象的目标方法,
// 以擷取到一個URL對象,然後對擷取到的URL對象進行空值檢查,為空也會抛出異常。
code.append(generateUrlAssignmentIndirectly(method));
}
// 這裡主要是擷取@Adaptive注解的參數,如果沒有配置,就會使用目标接口的類型由駝峰形式轉換為點分形式
// 的名稱作為将要擷取的參數值的key名稱,比如前面的PlantsWater轉換後為plants.water。
// 這裡需要注意的是,傳回值是一個數組類型,這是因為Dubbo會通過嵌套擷取的方式來的到目标參數,
// 比如我們使用了@Adaptive({"client", "transporter"})的形式,那麼最終就會在URL對象中擷取兩次
// 參數,如String extName = url.getParameter("client", url.getParameter("transporter"))
String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
// 判斷是否存在Invocation類型的參數
boolean hasInvocation = hasInvocationArgument(method);
// 為Invocation類型的參數添加空值檢查的邏輯
code.append(generateInvocationArgumentNullCheck(method));
// 生成擷取extName的邏輯,也即前面通過String[] value生成的通過url.getParameter()的
// 邏輯代碼,最終會得到使用者配置的擴充的名稱,進而對應某個基礎服務類
code.append(generateExtNameAssignment(value, hasInvocation));
// 為extName添加空值檢查代碼
code.append(generateExtNameNullCheck(value));
// 通過extName在ExtensionLoader中擷取其對應的基礎服務類,比如前面的PlantsWater,在這裡就是
// PlantsWater extension = ExtensionLoader.getExtensionLoader(ExtensionLoader.class)
// .getExtension(extName),這樣就得到了一個PlantsWater的執行個體對象
code.append(generateExtensionAssignment());
// 生成目标執行個體的目前方法的調用邏輯,然後将結果傳回。比如PlantsWater就是
// return extension.watering(arg0);
// 這裡方法名就是目前實作的方法的名稱,而參數就是目前方法傳入的參數,
// 就是目标接口中的同一方法,而方法參數前面已經講到,都是使用arg[i]的形式命名的,因而這裡直接
// 将其依次羅列出來即可
code.append(generateReturnAndInvocation(method));
}
// 将生成的代碼傳回
return code.toString();
}
上面的邏輯主要分為了如下幾個步驟:
- 判斷目前方法是否标注了@Adaptive注解,如果沒有标注,則為其生成一個預設實作,該實作中會預設抛出異常,也就是說隻有使用@Adaptive注解标注的方法才是作為自适應機制的方法;
- 擷取方法參數中類型為URL的參數,如果不存在,則擷取參數中某個存在可以傳回URL類型對象的方法的參數,并且調用該方法擷取URL參數;
- 通過@Adaptive注解的配置擷取目标參數的key值,然後通過前面得到的URL參數擷取該key對應的參數值,進而得到了基礎服務類對應的名稱;
- 通過ExtensionLoader擷取該名稱對應的基礎服務類執行個體;
-
通過調用基礎服務類的執行個體的目前方法來實作最終的基礎服務。
可以看到,這裡實作的自适應機制邏輯結構是非常清晰的,讀者通過閱讀這裡的源碼也就比較好的了解了Dubbo所提供的自适應機制的原理,也能夠比較好的通過自适應機制來完成某些定制化的工作。