前言
前面我們已經分析Dubbo SPI相關的源碼,看過的小夥伴相信已經知曉整個加載過程,我們也留下兩個問題,今天我們先來處理下其中關於註解Adaptive的原理。
什麼是@Adaptive
對應於Adaptive機制,Dubbo提供了一個註解@Adaptive,該註解可以用於接口的某個子類上,也可以用於接口方法上。如果用在接口的子類上,則表示Adaptive機制的實現會按照該子類的方式進行自定義實現;如果用在方法上,則表示Dubbo會爲該接口自動生成一個子類,並且重寫該方法,沒有標註@Adaptive註解的方法將會默認拋出異常。對於第一種Adaptive的使用方式,Dubbo裏只有ExtensionFactory接口使用,AdaptiveExtensionFactory的實現就使用了@Adaptive註解進行了標註,主要作用就是在獲取目標對象時,分別通過ExtensionLoader和Spring容器兩種方式獲取,該類的實現已經在Dubbo SPI機制分析過,此篇文章關注的重點是關於@Adaptive註解修飾在接口方法的實現原理,也就是關於Dubbo SPI動態的加載擴展類能力如何實現,搞清楚Dubbo是如何在運行時動態的選擇對應的擴展類來提供服務。簡單一點說就是一個代理層,通過對應的參數返回對應的類的實現,運行時編譯。爲了更好的理解我們來寫個案例:
@SPI("china")
public interface PersonService {
@Adaptive
String queryCountry(URL url);
}
public class ChinaPersonServiceImpl implements PersonService {
@Override
public String queryCountry(URL url) {
System.out.println("中國人");
return "中國人";
}
}
public class EnglandPersonServiceImpl implements PersonService{
@Override
public String queryCountry(URL url) {
System.out.println("英國人");
return "英國人";
}
}
public class Test {
public static void main(String[] args) {
URL url = URL.valueOf("dubbo://192.168.0.101:20880?person.service=china");
PersonService service = ExtensionLoader.getExtensionLoader(PersonService.class)
.getAdaptiveExtension();
service.queryCountry(url);
}
}
china=org.dubbo.spi.example.ChinaPersonServiceImpl
england=org.dubbo.spi.example.EnglandPersonServiceImpl
該案例中首先構造了一個URL對象,這個URL對象是Dubbo中進行參數傳遞所使用的一個基礎類,在配置文件中配置的屬性都會被封裝到該對象中。這裏我們需要注意的是我們的對象是通過一個url構造的,並且在url的最後有一個參數person.service=china,這裏也就是我們所指定的使用哪種基礎服務類的參數,通過指向不同的對象就可以生成對應不同的實現。關於URL部分的介紹我們在下一篇文章介紹,聊聊Dubbo中URL的使用場景有哪些。 在構造一個URL對象之後,通過getExtensionLoader(PersonService.class)方法獲取了一個PersonService對應的ExtensionLoader對象,然後調用其getAdaptiveExtension()方法獲取PersonService接口構造的子類實例,這裏的子類實際上就是ExtensionLoader通過一定的規則爲PersonService接口編寫的子類代碼,然後通過javassist或jdk編譯加載這段代碼,加載完成之後通過反射構造其實例,最後將其實例返回。當發生調用的時候,方法內部就會通過url對象指定的參數來選擇具體的實例,從而將真正的工作交給該實例進行。通過這種方式,Dubbo SPI就實現了根據傳入參數動態的選用具體的實例來提供服務的功能。以下代碼就是動態生成以後的代碼:
public class PersonService$Adaptive implements org.dubbo.spi.example.PersonService {
public java.lang.String queryCountry(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("person.service", "china");
if (extName == null)
throw new IllegalStateException("Failed to get extension (org.dubbo.spi.example.PersonService) name from url (" + url.toString() + ") use keys([person.service])");
org.dubbo.spi.example.PersonService extension = (org.dubbo.spi.example.PersonService) ExtensionLoader.getExtensionLoader(org.dubbo.spi.example.PersonService.class).getExtension(extName);
return extension.queryCountry(arg0);
}
}
關於使用我們需要注意以下兩個問題:
要使用Dubbo的SPI的支持,必須在目標接口上使用@SPI註解進行標註,後面的值提供了一個默認值,此處可以理解爲這是一種規範,如果在接口的@SPI註解中指定了默認值,那麼在使用URL對象獲取參數值時,如果沒有取到,就會使用該默認值; @Adaptive註解標註的方法中,其參數中必須有一個參數類型爲URL,或者其某個參數提供了某個方法,該方法可以返回一個URL對象,此處我們可以再看源碼的時候給大家標註一下,面試的時候防止大佬問:是不是一定要 @Adaptive 實現的方法的中必須有URL對象;
實現原理
getAdaptiveExtension
關於getAdaptiveExtension方法我們在上篇文章已經講過,此方法就是通過雙檢查法來從緩存中獲取Adaptive實例,如果沒獲取到,則創建一個。
public T getAdaptiveExtension() {
//從裝載適配器實例緩存裏面找
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
//創建cachedAdaptiveInstance異常
if (createAdaptiveInstanceError != null) {
throw new IllegalStateException("Failed to create adaptive instance: " +
createAdaptiveInstanceError.toString(),
createAdaptiveInstanceError);
}
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
//創建對應的適配器類
instance = createAdaptiveExtension();
//緩存
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
}
}
}
}
return (T) instance;
}
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);
}
}
getAdaptiveExtensionClass
在getAdaptiveExtensionClass方法中有兩個分支,如果某個子類標註了@Adaptive註解,那麼就會使用該子類所自定義的Adaptive機制,如果沒有子類標註該註解,那麼就會使用下面的createAdaptiveExtensionClass()方式來創建一個目標類class對象。整個過程通過AdaptiveClassCodeGenerator來爲目標類生成子類代碼,並以字符串的形式返回,最後通過javassist或jdk的方式進行編譯然後返回class對象。
private Class<?> getAdaptiveExtensionClass() {
//獲取所有的擴展類
getExtensionClasses();
//如果可以適配
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
//如果沒有適配擴展類就創建
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
private Class<?> createAdaptiveExtensionClass() {
//生成代碼片段
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
//獲取ClassLoader
ClassLoader classLoader = findClassLoader();
//通過jdk或者javassist的方式編譯生成的子類字符串,從而得到一個class對象
org.apache.dubbo.common.compiler.Compiler compiler =
ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
//編譯
return compiler.compile(code, classLoader);
}
generate
generate方法是生成目標類的方法,其實和創建一個類一樣,其主要四個步驟:
生成package信息; 生成import信息; 生成類聲明信息; 生成各個方法的實現;
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();
//生成package
code.append(generatePackageInfo());
//生成import信息 只導入了ExtensionLoader類,其餘的類都通過全限定名的方式來使用
code.append(generateImports());
//生成類聲明信息
code.append(generateClassDeclaration());
Method[] methods = type.getMethods();
//爲各個方法生成實現方法信息
for (Method method : methods) {
code.append(generateMethod(method));
}
code.append("}");
if (logger.isDebugEnabled()) {
logger.debug(code.toString());
}
//返回class代碼
return code.toString();
}
接下來主要看方法實現的生成,對於包路徑、類的生成的代碼相對比較簡單,這裏進行忽略,對於方法生成主要包含以下幾個步驟:
獲取返回值信息; 獲取方法名信息; 獲取方法體內容; 獲取方法參數; 獲取異常信息; 格式化
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);
}
需要注意的是,這裏所使用的所有類都是使用的其全限定類名,在上面生成的代碼中也可以看到,在方法生成的整個過程中,方法的返回值,方法名,方法參數以及異常信息都可以通過反射的信息獲取到,而方法體則需要根據一定規則來生成,這裏我們要看一下方法體是如何生成的;
private String generateMethodContent(Method method) {
//獲取Adaptive的註解信息
Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
StringBuilder code = new StringBuilder(512);
if (adaptiveAnnotation == null) {
//如果當前方法沒有被Adaptive修飾則需要拋出異常
return generateUnsupported(method);
} else {
//獲取參數中類型爲URL的參數所在的參數索引位 通過下標獲取對應的參數值信息
int urlTypeIndex = getUrlTypeIndex(method);
if (urlTypeIndex != -1) {
//如果參數中存在URL類型的參數,那麼就爲該參數進行空值檢查,如果爲空,則拋出異常
code.append(generateUrlNullCheck(urlTypeIndex));
} else {
//如果參數中不存在URL類型的參數,則會檢查每個參數,判斷是否有某個方法的返回類型是URL類型,
//如果存在該方法,首先對該參數進行空指針檢查,如果爲空則拋出異常。如果不爲空則調用該對象的目標方法,
//獲取URL對象,然後對獲取到的URL對象進行空值檢查,爲空拋出異常。
code.append(generateUrlAssignmentIndirectly(method));
}
//獲取@Adaptive註解的參數,如果沒有配置,就會使用目標接口的類型由駝峯形式轉換爲點分形式
//的名稱作爲將要獲取的參數值的key名稱
String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
//判斷是否存在Invocation類型的參數 關於這個對象我們在後續章節在進行講解
boolean hasInvocation = hasInvocationArgument(method);
//爲Invocation類型的參數添加空值檢查的邏輯
code.append(generateInvocationArgumentNullCheck(method));
//生成獲取extName的邏輯,獲取用戶配置的擴展的名稱
code.append(generateExtNameAssignment(value, hasInvocation));
//extName空值檢查代碼
code.append(generateExtNameNullCheck(value));
//通過extName在ExtensionLoader中獲取其對應的基礎服務類
code.append(generateExtensionAssignment());
//生成實例的當前方法的調用邏輯,然後將結果返回
code.append(generateReturnAndInvocation(method));
}
return code.toString();
}
上面整體的邏輯還是比較清楚的,通過對比PersonService$Adaptive生成我們可以更容易理解改代碼生成的過程,整體的邏輯可以分爲四步:
判斷當前方法是否標註了@Adaptive註解,如果沒有標註,則爲其生成默認拋出異常的方法,只有使用@Adaptive註解標註的方法纔是作爲自適應機制的方法; 獲取方法參數中類型爲URL的參數,如果不存在,則獲取參數中存在URL類型的參數,如果不存在拋出異常,如果存在獲取URL參數類型; 通過@Adaptive註解的配置獲取目標參數的key值,然後通過URL參數獲取該key對應的參數值,得到了基礎服務類對應的名稱; 通過ExtensionLoader獲取該名稱對應的基礎服務類實例,最終調用該服務的方法來進行實現;
結束
歡迎大家點點關注,點點贊!