擴展點開發指南

本文介紹 Dubbo SPI 的原理與實現細節

1 簡介

SPI 的全名是 Service Provider Interface,是一種服務發現機制。SPI 的本質是在檔案中配置介面實現類的全限定名,服務載入器讀取設定檔並載入實現類。通過這種方式,可以在執行時動態地替換介面的實現類。由於這個特性,我們可以通過 SPI 機制輕鬆地為程式提供擴充功能。SPI 機制也用於第三方框架中。例如,Dubbo 通過 SPI 機制載入所有元件。然而,Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了增強,以更好地滿足需求。在 Dubbo 中,SPI 是一個非常重要的模組。基於 SPI,我們可以輕鬆擴展 Dubbo。在 Dubbo 中,SPI 主要有兩種用法,一種是載入固定的擴充類,另一種是載入自適應的擴充類。下面將詳細介紹這兩種方法。需要注意的是:在 Dubbo 中,基於 SPI 擴充載入的類都是單例的。

1.1 載入固定擴充類

如果讓你設計並載入固定的擴充類,你會怎麼做?一個常見的思路是在特定目錄下讀取設定檔,然後解析出完整的類名,透過反射機制實例化類,再將類儲存在集合中。需要用的時候,直接從集合中取出。Dubbo 中的實現也是這樣的思路。不過,在 Dubbo 中,實現更加完善,它實現了 IOC 和 AOP 的功能。IOC 指的是,如果這個擴充類依賴其他的屬性,Dubbo 會自動注入這個屬性。這個功能是如何實現的呢?一個常見的思路是,取得這個擴充類的 setter 方法,呼叫 setter 方法進行屬性注入。AOP 指的是什麼呢?指的是 Dubbo 可以將它的包裝類注入到擴充類中。例如,DubboProtocol 是 Protocol 的一個擴充類,ProtocolListenerWrapper 是 DubboProtocol 的一個包裝類。

1.2 載入自適應擴充類

首先說明一下自適應擴展類的使用場景。例如,我們有一個需求,在調用某個方法時,可以根據參數的選擇調用不同的實現類。有點類似工廠方法,根據不同的參數構造不同的實例對象。Dubbo 中的實現思路與此類似,但 Dubbo 的實現更加靈活,它的實現方式有點類似策略模式。每個擴展類相當於一個策略,基於 URL 消息總線,將參數傳遞給 ExtensionLoader,通過 ExtensionLoader 根據參數加載相應的擴展類,實現運行時對目標實例的動態調用。

2. Dubbo SPI 源碼分析

2.1 加載固定的擴展類

在 Dubbo 中,SPI 加載固定擴展類的入口是 ExtensionLoader 的 getExtension 方法。接下來,我們將詳細分析獲取擴展類對象的過程。

public T getExtension(String name) {
    if (name == null || name. length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    if ("true".equals(name)) {
        // Get the default extension implementation class
        return getDefaultExtension();
    }
    // Holder, as the name suggests, is used to hold the target object
    Holder<Object> holder = cachedInstances. get(name);
    // This logic ensures that only one thread can create a Holder object
    if (holder == null) {
        cachedInstances. putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances. get(name);
    }
    Object instance = holder. get();
    // double check
    if (instance == null) {
        synchronized (holder) {
            instance = holder. get();
            if (instance == null) {
                // create extension instance
                instance = createExtension(name);
                // set the instance to the holder
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

上面程式碼的邏輯比較簡單,首先檢查緩存,如果緩存未命中,則創建擴展對象。我們來看看創建擴展對象的過程。

private T createExtension(String name, boolean wrap) {
    // Load all extension classes from the configuration file to get the mapping relationship table from "configuration item name" to "configuration class"
    Class<?> clazz = getExtensionClasses().get(name);
    // If there is no extension of the interface, or the implementation class of the interface does not allow repetition but actually repeats, an exception is thrown directly
    if (clazz == null || unacceptableExceptions. contains(name)) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES. get(clazz);
        // This code ensures that the extended class will only be constructed once, which is a singleton.
        if (instance == null) {
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
            instance = (T) EXTENSION_INSTANCES. get(clazz);
        }
        // Inject dependencies into the instance
        injectExtension(instance);

        // Automatically wrap if wrapping is enabled.
        // For example, I defined the extension of DubboProtocol based on Protocol, but in fact, DubboProtocol is not directly used in Dubbo, but its wrapper class
        // ProtocolListenerWrapper
        if (wrap) {

            List<Class<?>> wrapperClassesList = new ArrayList<>();
            if (cachedWrapperClasses != null) {
                wrapperClassesList.addAll(cachedWrapperClasses);
                wrapperClassesList.sort(WrapperComparator. COMPARATOR);
                Collections. reverse(wrapperClassesList);
            }
    
            // Loop to create Wrapper instances
            if (CollectionUtils. isNotEmpty(wrapperClassesList)) {
                for (Class<?> wrapperClass : wrapperClassesList) {
                    Wrapper wrapper = wrapperClass. getAnnotation(Wrapper. class);
                    if (wrapper == null
                            || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
                        // Pass the current instance as a parameter to the constructor of Wrapper, and create a Wrapper instance through reflection.
                        // Then inject dependencies into the Wrapper instance, and finally assign the Wrapper instance to the instance variable again
                        instance = injectExtension((T) wrapperClass. getConstructor(type). newInstance(instance));
                    }
                }
            }
        }
        // initialization
        initExtension(instance);
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                type + ") couldn't be instantiated: " + t. getMessage(), t);
    }
}

createExtension 方法的邏輯稍微複雜一些,包含以下幾個步驟:

  1. 通過 getExtensionClasses 獲取所有擴展類
  2. 通過反射創建擴展對象
  3. 向擴展對象注入依賴
  4. 將擴展對象包裝到對應的 Wrapper 對象中
  5. 初始化擴展對象

在以上步驟中,第一步是加載擴展類的關鍵,第三步和第四步是 Dubbo IOC 和 AOP 的具體實現。在後面的章節中,我們將重點分析 getExtensionClasses 方法的邏輯,並簡要介紹 Dubbo IOC 的具體實現。

2.1.1 獲取所有擴展類

在我們根據名稱獲取擴展類之前,首先需要根據配置文件解析出擴展項名稱到擴展類的映射關係表(Map<名稱, 擴展類>),然後再根據擴展項名稱從映射關係表中取出對應的擴展類即可。相關過程的程式碼分析如下:

private Map<String, Class<?>> getExtensionClasses() {
    // Get the loaded extension class from the cache
    Map<String, Class<?>> classes = cachedClasses. get();
    // double check
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses. get();
            if (classes == null) {
                // load extension class
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

這裡也是先檢查緩存,如果緩存未命中,則通過 synchronized 加鎖。加鎖後再次檢查緩存,發現為空。此時如果 classes 仍然為 null,則通過 loadExtensionClasses 加載擴展類。我們來分析一下 loadExtensionClasses 方法的邏輯。

private Map<String, Class<?>> loadExtensionClasses() {
    // Cache the default SPI extension
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();
    
    // Load the files in the specified folder based on the policy
    // Currently there are four strategies to read the configuration files in META-INF/services/ META-INF/dubbo/ META-INF/dubbo/internal/ META-INF/dubbo/external/ respectively
    for (LoadingStrategy strategy : strategies) {
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }

    return extensionClasses;
}

loadExtensionClasses 方法總共做了兩件事情,一是解析 SPI 註解,二是調用 loadDirectory 方法加載指定資料夾的配置文件。解析 SPI 註解的過程比較簡單,這裡就不多說了。我們來看看 loadDirectory 做了什麼。

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
                           boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
    // fileName = folder path + type fully qualified name
    String fileName = dir + type;
    try {
        Enumeration<java.net. URL> urls = null;
        ClassLoader classLoader = findClassLoader();

        // try to load from ExtensionLoader's ClassLoader first
        if (extensionLoaderClassLoaderFirst) {
            ClassLoader extensionLoaderClassLoader = ExtensionLoader. class. getClassLoader();
            if (ClassLoader. getSystemClassLoader() != extensionLoaderClassLoader) {
                urls = extensionLoaderClassLoader. getResources(fileName);
            }
        }
        // Load all files with the same name according to the file name
        if (urls == null || !urls.hasMoreElements()) {
            if (classLoader != null) {
                urls = classLoader. getResources(fileName);
            } else {
                urls = ClassLoader. getSystemResources(fileName);
            }
        }

        if (urls != null) {
            while (urls. hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                // load resources
                loadResource(extensionClasses, classLoader, resourceURL, overridden, excludedPackages);
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                type + ", description file: " + fileName + ").", t);
    }
}

loadDirectory 方法首先通過 classLoader 獲取所有資源連結,然後通過 loadResource 方法加載資源。我們繼續來看 loadResource 方法的實現。

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
                          java.net.URL resourceURL, boolean overridden, String... excludedPackages) {
    try {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL. openStream(), StandardCharsets. UTF_8))) {
            String line;
            String clazz = null;
            // Read configuration content line by line
            while ((line = reader. readLine()) != null) {
                // locate # characters
                final int ci = line. indexOf('#');
                if (ci >= 0) {
                    // Intercept the string before #, the content after # is a comment, which needs to be ignored
                    line = line. substring(0, ci);
                }
                line = line. trim();
                if (line. length() > 0) {
                    try {
                        String name = null;
                        // Use the equal sign = as the boundary to intercept the key and value
                        int i = line. indexOf('=');
                        if (i > 0) {
                            name = line.substring(0, i).trim();
                            clazz = line.substring(i + 1).trim();
                        } else {
                            clazz = line;
                        }
                        // Load the class and cache the class through the loadClass method
                        if (StringUtils.isNotEmpty(clazz) && !isExcluded(clazz, excludedPackages)) {
                            loadClass(extensionClasses, resourceURL, Class. forName(clazz, true, classLoader), name, overridden);
                        }
                    } catch (Throwable t) {
                        IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
                        exceptions. put(line, e);
                    }
                }
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                type + ", class file: " + resourceURL + ") in " + resourceURL, t);
    }
}

loadResource 方法用於讀取和解析配置文件,通過反射加載類別,最後調用 loadClass 方法進行其他操作。loadClass 方法主要用於操作緩存,該方法的邏輯如下:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                       boolean overridden) throws NoSuchMethodException {
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("Error occurred when loading extension class (interface: " +
                type + ", class line: " + clazz. getName() + "), class "
                + clazz.getName() + " is not subtype of interface.");
    }
    // Check if there are Adaptive annotations on the target class
    if (clazz. isAnnotationPresent(Adaptive. class)) {
        cacheAdaptiveClass(clazz, overridden);
    } else if (isWrapperClass(clazz)) {
        // cache wrapper class
        cacheWrapperClass(clazz);
    } else {
        // Enter here, indicating that this class is just an ordinary extension class
        // Check if clazz has a default constructor, if not, throw an exception
        clazz. getConstructor();
        if (StringUtils. isEmpty(name)) {
            // If name is empty, try to get name from Extension annotation, or use lowercase class name as name
            name = findAnnotationName(clazz);
            if (name. length() == 0) {
                throw new IllegalStateException("No such extension name for the class " + clazz. getName() + " in the config " + resourceURL);
            }
        }

        String[] names = NAME_SEPARATOR. split(name);
        if (ArrayUtils. isNotEmpty(names)) {
            // If the class has the Activate annotation, use the first element of the names array as the key,
            // Store the mapping relationship between name and Activate annotation object
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                // Store the mapping relationship from Class to name
                cacheName(clazz, n);
                // Store the mapping relationship from name to Class.
                // If there are multiple implementation classes corresponding to the same extension, whether overriding is allowed based on the override parameter, if not, an exception is thrown.
                saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
        }
    }
}

如上所示,loadClass 方法操作不同的緩存,例如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等。除此之外,該方法沒有其他邏輯。

至此,類別加載緩存過程的分析就結束了。整個過程沒有什麼特別複雜的地方,可以一步一步分析,不懂的地方可以除錯一下。接下來,我們來聊聊 Dubbo IOC。

2.1.2 Dubbo IOC

Dubbo IOC 透過 setter 方法注入依賴。Dubbo 首先透過反射取得實例的所有方法,然後遍歷方法清單,檢測方法名稱是否具有 setter 方法的特徵。如果是,則透過 ObjectFactory 取得依賴物件,最後透過反射呼叫 setter 方法,將依賴設定到目標物件中。整個過程對應的程式碼如下

private T injectExtension(T instance) {

    if (objectFactory == null) {
        return instance;
    }

    try {
        // traverse all methods of the target class
        for (Method method : instance. getClass(). getMethods()) {
            // Check whether the method starts with set, and the method has only one parameter, and the method access level is public
            if (!isSetter(method)) {
                continue;
            }
            /**
             * Detect whether there is DisableInject annotation modification.
             */
            if (method. getAnnotation(DisableInject. class) != null) {
                continue;
            }
            
            /**
             * Detect whether the ScopeModelAware and ExtensionAccessorAware classes are implemented, and if implemented, do not inject
             */
            if (method. getDeclaringClass() == ScopeModelAware. class) {
                    continue;
            }
            if (instance instanceof ScopeModelAware || instance instanceof ExtensionAccessorAware) {
                if (ignoredInjectMethodsDesc. contains(ReflectUtils. getDesc(method))) {
                    continue;
                }
            }
            
            // Primitive types are not injected
            Class<?> pt = method. getParameterTypes()[0];
            if (ReflectUtils. isPrimitives(pt)) {
                continue;
            }

            try {
                // Get the attribute name, for example, the setName method corresponds to the attribute name name
                String property = getSetterProperty(method);
                // Get dependent objects from ObjectFactory
                Object object = objectFactory. getExtension(pt, property);
                if (object != null) {
                    // inject
                    method.invoke(instance, object);
                }
            } catch (Exception e) {
                logger.error("Failed to inject via method " + method.getName()
                        + " of interface " + type. getName() + ": " + e. getMessage(), e);
            }

        }
    } catch (Exception e) {
        logger. error(e. getMessage(), e);
    }
    return instance;
}

在上述程式碼中,objectFactory 變數的類型為 AdaptiveExtensionFactory,AdaptiveExtensionFactory 內部維護了一個 ExtensionFactory 清單,用於存放其他類型的 ExtensionFactory。Dubbo 目前提供了兩種 ExtensionFactory,分別是 SpiExtensionFactory 和 SpringExtensionFactory。前者用於建立自適應擴展,後者用於從 Spring 的 IOC 容器中取得所需的擴展。這兩個類的程式碼並不複雜,這裡就不逐一分析了。

Dubbo IOC 目前只支援 setter 注入。總體來說,邏輯比較簡單,容易理解。

2.2 載入自適應擴展類

自適應擴展類的含義是,基於參數,在執行時動態選擇某個具體的目標類,然後執行。在 Dubbo 中,很多擴展都是透過 SPI 機制載入的,例如 Protocol、Cluster、LoadBalance 等。有時候,有些擴展不希望在框架啟動階段就被載入,而是希望在呼叫擴展方法的時候,根據執行時參數進行載入。這聽起來有點矛盾,如果擴展沒有被載入,那麼擴展方法就無法被呼叫(靜態方法除外);如果擴展方法沒有被呼叫,那麼擴展就無法被載入。對於這個矛盾的問題,Dubbo 透過自適應擴展機制很好地解決了。自適應擴展機制的實現邏輯比較複雜,首先 Dubbo 會為擴展介面生成具有代理功能的程式碼。然後透過 javassist 或 jdk 編譯這段程式碼,得到 Class 類。最後透過反射建立代理類,整個流程比較繁瑣。

載入自適應擴展類的入口是 ExtensionLoader 的 getAdaptiveExtension 方法。

public T getAdaptiveExtension() {
    // Get the adaptive extension from the cache
    Object instance = cachedAdaptiveInstance. get();
    if (instance == null) {
        // If there is an exception, throw it directly
        if (createAdaptiveInstanceError != null) {
            throw new IllegalStateException("Failed to create adaptive instance: " +
                    createAdaptiveInstanceError.toString(),
                    createAdaptiveInstanceError);
        }

        synchronized (cachedAdaptiveInstance) {
            instance = cachedAdaptiveInstance. get();
            // double check
            if (instance == null) {
                try {
                    // create adaptive extension
                    // There are two cases here: one is that there is an Adaptive class, and the other is that an Adaptive class needs to be generated
                    instance = createAdaptiveExtension();
                    cachedAdaptiveInstance.set(instance);
                } catch (Throwable t) {
                    createAdaptiveInstanceError = t;
                    throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
                }
            }
        }
    }

    return (T) instance;
}

getAdaptiveExtension 方法會先檢查快取,如果快取未命中,則呼叫 createAdaptiveExtension 方法建立自適應擴展。接下來,我們來看一下 createAdaptiveExtension 方法的程式碼。

private T createAdaptiveExtension() {
    try {
        // Get the adaptive extension class and instantiate it through reflection
        return injectExtension((T) getAdaptiveExtensionClass(). newInstance());
    } catch (Exception e) {
        throw new IllegalStateException("Can not create adaptive extension ...");
    }
}

createAdaptiveExtension 方法的程式碼比較少,但它包含了三個邏輯,分別是

  1. 呼叫 getAdaptiveExtensionClass 方法取得自適應擴展 Class 物件
  2. 透過反射進行實例化
  3. 呼叫 injectExtension 方法,為擴展實例注入依賴

前兩個邏輯比較容易理解,第三個邏輯是用於為自適應擴展物件注入依賴。這個邏輯看起來似乎是多餘的,但它是必須存在的。這裡簡單解釋一下,前面提到,Dubbo 中的自適應擴展有兩種,一種是手動編寫程式碼的,另一種是自動生成的。手動編寫程式碼的 Adaptive 擴展中可能會存在一些依賴,而自動生成的 Adaptive 擴展則不會依賴其他類。這裡呼叫 injectExtension 方法的目的就是為手動編寫程式碼的自適應擴展注入依賴,這一點需要大家注意。關於 injectExtension 方法,在前面的文章中已經分析過了,這裡就不再贅述了。接下來,分析 getAdaptiveExtensionClass 方法的邏輯。

private Class<?> getAdaptiveExtensionClass() {
    // Get all extension classes through SPI
    getExtensionClasses();
    // Check the cache, if the cache is not empty, return the cache directly
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    // Create an adaptive extension class
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

getAdaptiveExtensionClass 方法也包含了三個邏輯,分別是

  1. 呼叫 getExtensionClasses 方法取得所有擴展類
  2. 檢查快取,如果快取不為空,則返回快取
  3. 如果快取為空,則呼叫 createAdaptiveExtensionClass 方法建立自適應擴展類

這三個邏輯看似平平無奇,好像沒什麼好講的。但這平淡的程式碼中卻隱藏著一些細節需要說明。首先,我們從第一個邏輯入手。getExtensionClasses 方法用於獲取一個介面的所有實現類。例如,該方法可以取得 Protocol 介面的 DubboProtocol、HttpProtocol、InjvmProtocol 等實現類。在獲取實現類的過程中,如果某個實現類被 Adaptive 註解修飾,則會將該類賦值給 cachedAdaptiveClass 變數。此時,滿足上述步驟中第二步的條件(快取不為空),直接返回 cachedAdaptiveClass 即可。如果所有實現類都沒有被 Adaptive 註解修飾,則執行第三步的邏輯,創建自適應擴展類。相關程式碼如下

private Class<?> createAdaptiveExtensionClass() {
    // Build adaptive extension code
    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
    ClassLoader classLoader = findClassLoader();
    // Get the compiler implementation class
    org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    // Compile the code and generate Class
    return compiler.compile(code, classLoader);
}

createAdaptiveExtensionClass 方法用於生成自適應擴展類。該方法首先會生成自適應擴展類的原始碼,然後通過 Compiler 實例(Dubbo 預設使用 javassist 作為編譯器)編譯原始碼,得到一個代理類 Class 實例。接下來,我們將重點關注代理類程式碼生成的邏輯,其他邏輯自行分析。

2.2.1 自適應擴展類程式碼生成

AdaptiveClassCodeGenerator#generate 方法生成擴展類程式碼

public String generate() {
    // If there is no method in the interface modified by the @Adaptive annotation, an exception is thrown directly
    if (!hasAdaptiveMethod()) {
        throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
    }

    StringBuilder code = new StringBuilder();
    // Generate package name, import, method, etc.
    code.append(generatePackageInfo());
    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());
    }
    return code. toString();
}

2.2.2 生成方法

在上面的程式碼中,生成方法的邏輯是最關鍵的,我們來詳細分析一下。

private String generateMethod(Method method) {
    String methodReturnType = method. getReturnType(). getCanonicalName();
    String methodName = method. getName();
    // generate method content
    String methodContent = generateMethodContent(method);
    String methodArgs = generateMethodArguments(method);
    String methodThrows = generateMethodThrows(method);
    return String. format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
}

generateMethodContent 分析

private String generateMethodContent(Method method) {
    // This method must be decorated with @Adaptive annotation
    Adaptive adaptiveAnnotation = method. getAnnotation(Adaptive. class);
    StringBuilder code = new StringBuilder(512);
    if (adaptiveAnnotation == null) {
        // Without @Adaptive annotation modification, exception information is generated
        return generateUnsupported(method);
    } else {
        // Get the index of the URL on the parameter list
        int urlTypeIndex = getUrlTypeIndex(method);
        
        if (urlTypeIndex != -1) {
            // Generate a null check for the URL if it exists on the parameter list
            code.append(generateUrlNullCheck(urlTypeIndex));
        } else {
            // If there is no parameter of URL type in the parameter list, then it depends on whether the parameter object on the parameter list contains the getUrl method
            // If there is, generate a URL null check
            code.append(generateUrlAssignmentIndirectly(method));
        }
        // parse the value attribute on the Adaptive annotation
        String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
        // If there is a parameter of type Invocation on the parameter list, generate a null check and get the methodName.
        boolean hasInvocation = hasInvocationArgument(method);
        
        code.append(generateInvocationArgumentNullCheck(method));
        // This logic is mainly to generate extName (that is, the extension)
        // Divided into multiple situations:
        // 1. Does defaultExtName exist?
        // 2. Whether there is an invocation type parameter in the parameter
        // 3. Whether to generate a proxy for the protocol
        // Why should the protocol be considered separately? Because there is a method to get the protocol value in the URL
        code.append(generateExtNameAssignment(value, hasInvocation));
        // check extName == null?
        code.append(generateExtNameNullCheck(value));
    
        // generate get extension (using ExtensionLoader.getExtension method)
        code.append(generateExtensionAssignment());

        // generate return statement
        code.append(generateReturnAndInvocation(method));
    }

    return code. toString();
}

上述邏輯主要做了以下幾件事情

  1. 檢查方法上是否被 Adaptive 註解修飾
  2. 生成方法程式碼時,參數列表中必須要有 URL(或者參數物件中有 URL)
  3. 使用 ExtensionLoader.getExtension 獲取擴展實現
  4. 執行對應的方法

2.2.3 附上一段動態生成的程式碼示例

package org.apache.dubbo.common.extension.adaptive;

import org.apache.dubbo.common.extension.ExtensionLoader;


public class HasAdaptiveExt$Adaptive implements org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt {
    public java.lang.String echo(org.apache.dubbo.common.URL arg0,
        java. lang. String arg1) {
        // URL null check
        if (arg0 == null) {
            throw new IllegalArgumentException("url == null");
        }

        org.apache.dubbo.common.URL url = arg0;
        // get the extension
        String extName = url. getParameter("has. adaptive. ext", "adaptive");
        // extension null check
        if (extName == null) {
            throw new IllegalStateException(
                "Failed to get extension (org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt) name from url (" +
                url.toString() + ") use keys([has.adaptive.ext])");
        }
        // get extension
        org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt extension = (org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt) ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt.class)
                                                                                                                                                         .getExtension(extName);
        // Execute the corresponding method
        return extension.echo(arg0, arg1);
    }
}

3. SPI 擴展範例

3.1 載入固定的擴展類

3.1.1 撰寫 SPI 介面及實現類

無論是 Java SPI 還是 Dubbo 中實現的 SPI,都需要撰寫一個介面。不過 Dubbo 中的介面需要使用 @SPI 註解修飾。

@SPI
public interface DemoSpi {
    void say();
}

public class DemoSpiImpl implements DemoSpi {
    public void say() {
    }
}

3.1.2 將實現類放置在特定目錄下

從上面的程式碼中我們可以看到,dubbo 載入擴展類時,會從四個目錄中讀取。我們在 META-INF/dubbo 目錄下新建一個以 DemoSpi 介面命名的檔案,內容如下

demoSpiImpl = com.xxx.xxx.DemoSpiImpl (full class name for the implementation class of the DemoSpi interface)

3.1.3 使用

public class DubboSPITest {

    @Test
    public void sayHello() throws Exception {
        ExtensionLoader<DemoSpi> extensionLoader =
            ExtensionLoader. getExtensionLoader(DemoSpi. class);
        DemoSpi dmeoSpi = extensionLoader. getExtension("demoSpiImpl");
        optimusPrime. sayHello();
    }
}

3.2 載入自適應擴展類

這裡以 Protocol 為例說明

3.2.1 Protocol 介面(摘取部分核心方法)

@SPI("dubbo")
public interface Protocol {
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
}

public class DubboProtocol extends AbstractProtocol {
     …
    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        return protocolBindingRefer(type, url);
    }
    
    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
         …
        return exporter;
    }
}

3.2.2 將實現類放置在特定目錄下

在 dubbo 中,配置路徑為 META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol

dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

需要注意的是,在 dubbo 中,並不會直接使用 DubboProtocol,而是使用它的包裝類。

3.2.3 使用

public class DubboAdaptiveTest {

    @Test
    public void sayHello() throws Exception {
        URL url = URL.valueOf("dubbo://#/test");
        Protocol adaptiveProtocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
        adaptiveProtocol. refer(type, url);
    }
}

最後修改日期:2023 年 1 月 2 日:增強英文文件 (#1798) (95a9f4f6c1c)