Dubbo-Adaptive實現原理

2022-09-05 15:01:49

前言

前面我們已經分析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 == nullthrow 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);
    }
}

關於使用我們需要注意以下兩個問題:

  1. 要使用Dubbo的SPI的支援,必須在目標介面上使用@SPI註解進行標註,後面的值提供了一個預設值,此處可以理解為這是一種規範,如果在介面的@SPI註解中指定了預設值,那麼在使用URL物件獲取引數值時,如果沒有取到,就會使用該預設值;
  2. @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方法是生成目標類的方法,其實和建立一個類一樣,其主要四個步驟:

  1. 生成package資訊;
  2. 生成import資訊;
  3. 生成類宣告資訊;
  4. 生成各個方法的實現;
    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();
    }

接下來主要看方法實現的生成,對於包路徑、類的生成的程式碼相對比較簡單,這裡進行忽略,對於方法生成主要包含以下幾個步驟:

  1. 獲取返回值資訊;
  2. 獲取方法名資訊;
  3. 獲取方法體內容;
  4. 獲取方法引數;
  5. 獲取異常資訊;
  6. 格式化
    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生成我們可以更容易理解改程式碼生成的過程,整體的邏輯可以分為四步:

  1. 判斷當前方法是否標註了@Adaptive註解,如果沒有標註,則為其生成預設丟擲異常的方法,只有使用@Adaptive註解標註的方法才是作為自適應機制的方法;
  2. 獲取方法引數中型別為URL的引數,如果不存在,則獲取引數中存在URL型別的引數,如果不存在丟擲異常,如果存在獲取URL引數型別;
  3. 通過@Adaptive註解的設定獲取目標引數的key值,然後通過URL引數獲取該key對應的引數值,得到了基礎服務類對應的名稱;
  4. 通過ExtensionLoader獲取該名稱對應的基礎服務類範例,最終呼叫該服務的方法來進行實現;

結束

歡迎大家點點關注,點點贊!