Dubbo 擴展點加載機制:從 Java SPI 到 Dubbo SPI

SPI 全稱為 Service Provider Interface,是一種服務發現機制。當程序運行調用接口時,會根據配置文件或默認規則信息加載對應的實現類。所以在程序中並沒有直接指定使用接口的哪個實現,而是在外部進行裝配。 要想了解 Dubbo 的設計與實現,其中 Dubbo SPI 加載機制是必須瞭解的,在 Dubbo 中有大量功能的實現都是基於 Dubbo SPI 實現解耦,同時也使得 Dubbo 獲得如此好的可擴展性。

Java SPI

通過完成一個 Java SPI 的操作來了解它的機制。

  • 創建一個 AnimalService 接口及 category 方法
  • 創建一個實現類 Cat
  • 創建 META-INF/services 目錄,並在該目錄下創建一個文件,文件名為 AnimalService 的全限定名作為文件名
  • 在文件中添加實現類 Cat 的全限定名

Animal 接口

<code>public interface AnimalService {
    void category();
}/<code>

Cat 實現類

<code>public class Cat implements AnimalService {
    @Override
    public void category() {
        System.out.println("cat: Meow ~");
    }
}/<code>

在 META-INF/services 目錄下的 top.ytao.demo.spi.AnimalService 文件中添加:

top.ytao.demo.spi.Cat

加載 SPI 的實現:

<code>public class JavaSPITest {
    @Test
    public void javaSPI() throws Exception {
        ServiceLoader<animalservice> serviceLoader = ServiceLoader.load(AnimalService.class);

        // 遍歷在配置文件中已配置的 AnimalService 的所有實現類
        for (AnimalService animalService : serviceLoader) {
            animalService.category();
        }
    }
}/<animalservice>/<code>

執行結果:

Dubbo 擴展點加載機制:從 Java SPI 到 Dubbo SPI


就這樣,一個 Java SPI 就實現完成了,通過ServiceLoader.load獲取加載所有接口已配置的接口實現類,然後可以遍歷找出需要的實現。

Dubbo SPI

本文 Dubbo 版本為2.7.5

Dubbo SPI 相較於 Java SPI 更為強大,並且都是由自己實現的一套 SPI 機制。其中主要的改進和優化:

  • 相對於 Java SPI 一次性加載所有實現,Dubbo SPI 是按需加載,只加載需要使用的實現類。同時帶有緩存支持。
  • 更為詳細的擴展加載失敗信息。
  • 增加了對擴展 IOC 和 AOP的支持。

Dubbo SPI 示例

Dubbo SPI 的配置文件放在 META-INF/dubbo 下面,並且實現類的配置方式採用 K-V 的方式,key 為實例化對象傳入的參數,value 為擴展點實現類全限定名。例如 Cat 的配置文件內容:

cat = top.ytao.demo.spi.Cat

Dubbo SPI 加載過程中,對 Java SPI 的目錄也是可以被兼容的。

同時需要在接口上增加 @SPI 註解,@SPI 中可以指定 key 值,加載 SPI 如下:

<code>public class DubboSPITest {
    @Test
    public void dubboSPI(){
        ExtensionLoader<animalservice> extensionLoader = ExtensionLoader.getExtensionLoader(AnimalService.class);
        // 獲取擴展類實現
        AnimalService cat = extensionLoader.getExtension("cat");
        System.out.println("Dubbo SPI");
        cat.category();
    }
}/<animalservice>/<code>

執行結果如下:

Dubbo 擴展點加載機制:從 Java SPI 到 Dubbo SPI

獲取 ExtensionLoader 實例

獲取 ExtensionLoader 實例是通過上面 getExtensionLoader 方法,具體實現代碼:

<code>public static  ExtensionLoader getExtensionLoader(Class type) {
    if (type == null) {
        throw new IllegalArgumentException("Extension type == null");
    }
    // 檢查 type 必須為接口
    if (!type.isInterface()) {
        throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
    }
    // 檢查接口是否有 SPI 註解
    if (!withExtensionAnnotation(type)) {
        throw new IllegalArgumentException("Extension type (" + type +
                ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
    }
    // 緩存中獲取 ExtensionLoader 實例
    ExtensionLoader loader = (ExtensionLoader) EXTENSION_LOADERS.get(type);
    if (loader == null) {
        // 加載 ExtensionLoader 實例到緩存中
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
        loader = (ExtensionLoader) EXTENSION_LOADERS.get(type);
    }
    return loader;
}
/<code>

上面獲取擴展類加載器過程主要是檢查傳入的 type 是否合法,以及從擴展類加載器緩存中是否存在當前類型的接口,如果不存在則添加當前接口至緩存中。ConcurrentMap<class>,ExtensionLoader>>EXTENSION_LOADERS是擴展類加載器的緩存,它是以接口作為 key, 擴展類加載器作為 value 進行緩存。/<class>

獲取擴展類對象

獲取擴展類對象的方法ExtensionLoader#getExtension,在這裡完成擴展對象的緩存及創建工作:

<code>public T getExtension(String name) {
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    // 如果傳入的參數為 true ,則獲取默認擴展類對象操作
    if ("true".equals(name)) {
        return getDefaultExtension();
    }
    // 獲取擴展對象,Holder 裡的 value 屬性保存著擴展對象實例
    final Holder<object> holder = getOrCreateHolder(name);
    Object instance = holder.get();
    // 使用雙重檢查鎖
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // 創建擴展對象
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}/<object>/<code>

獲取 holder 對象是從緩存ConcurrentMap<string>>cachedInstances中獲取,如果不存在,則以擴展名 key,創建一個 Holder 對象作為 value,設置到擴展對象緩存。 如果是新創建的擴展對象實例,那麼 holder.get() 一定是 null ,擴展對象為空時,經過雙重檢查鎖,創建擴展對象。/<string>

創建擴展對象

創建擴展對象過程:

<code>private T createExtension(String name) {
    // 從全部擴展類中,獲取當前擴展名對應的擴展類
    Class> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        // 從緩存中獲取擴展實例,及設置擴展實例緩存
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // 向當前實例注入依賴
        injectExtension(instance);
        // 獲取包裝擴展類緩存
        Set<class>> wrapperClasses = cachedWrapperClasses;
        if (CollectionUtils.isNotEmpty(wrapperClasses)) {
            for (Class> wrapperClass : wrapperClasses) {
                // 創建包裝擴展類實例,並向其注入依賴
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        // 初始化擴展對象
        initExtension(instance);
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                type + ") couldn't be instantiated: " + t.getMessage(), t);
    }
}/<class>/<code>

上面創建擴展過程中,裡面有個 Wrapper 類,這裡使用到裝飾器模式,該類是沒有具體的實現,而是把通用邏輯進行抽象。 創建這個過程是從所有擴展類中獲取當前擴展名對應映射關係的擴展類,以及向當前擴展對象注入依賴。

獲取所有擴展類:

<code>private Map<string>> getExtensionClasses() {
    // 獲取普通擴展類緩存
    Map<string>> classes = cachedClasses.get();
    // 如果緩存中沒有,通過雙重檢查鎖後進行加載
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 加載全部擴展類
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}/<string>/<string>/<code>

檢查普通擴展類緩存是否為空,如果不為空則重新加載,真正加載擴展類在loadExtensionClasses中:

<code>private static final String SERVICES_DIRECTORY = "META-INF/services/";
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
private Map<string>> loadExtensionClasses() {
    // 獲取 @SPI 上的默認擴展名
    cacheDefaultExtensionName();
    Map<string>> extensionClasses = new HashMap<>();
    // 先加載 Dubbo 內部的擴展類, 通過 Boolean 值控制
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName(), true);
    // 由於 Dubbo 遷到 apache ,所以包名有變化,會替換之前的 alibaba 為 apache
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"), true);
    loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
    loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
    return extensionClasses;

}/<string>/<string>/<code>

上面獲取 @SPI 擴展名,以及指定要加載的文件。從上面靜態常量中,我們可以看到,Dubbo SPI 也是支持加載 Java SPI 的目錄,同時還加載 META-INF/dubbo/internal (該目錄為 Dubbo 的內部擴展類目錄),在 loadDirectory 加載目錄配置文件。

<code>    private void loadDirectory(Map<string>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst) {
        // 獲取文件在項目中的路徑,如:META-INF/dubbo/top.ytao.demo.spi.AnimalService
        String fileName = dir + type;
        try {
            Enumeration<java.net.url> urls = null;
            ClassLoader classLoader = findClassLoader();
            // 加載內部擴展類
            if (extensionLoaderClassLoaderFirst) {
                ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
                if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                    urls = extensionLoaderClassLoader.getResources(fileName);
                }
            }
            // 加載當前 fileName 文件
            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();
                    // 加載文件內容
                    loadResource(extensionClasses, classLoader, resourceURL);
                }
            }
        } catch (Throwable t) {
            logger.error("Exception occurred when loading extension class (interface: " +
                    type + ", description file: " + fileName + ").", t);
        }
    }/<java.net.url>/<string>/<code>

這裡獲取文件名後加載所有同名文件,然後迭代各個文件,逐個加載文件內容。

<code>private void loadResource(Map<string>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
    try {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
            String line;
            // 整行讀取文件內容
            while ((line = reader.readLine()) != null) {
                // 獲取當前行中第一個 "#" 的位置索引
                final int ci = line.indexOf('#');
                // 如果當前行存在 "#",則去除 "#" 後的內容
                if (ci >= 0) {
                    line = line.substring(0, ci);
                }
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        String name = null;
                        // 獲取當前行 "=" 的索引
                        int i = line.indexOf('=');
                        // 如果當前行存在 "=",將 "=" 左右的值分開復制給 name 和 line
                        if (i > 0) {
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            // 加載擴展類
                            loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                        }
                    } 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);
    }

}/<string>/<code>

上面代碼完成文件內容加載和解析,接下來通過loadClass加載擴展類。

<code>private void loadClass(Map<string>> extensionClasses, java.net.URL resourceURL, Class> clazz, String name) throws NoSuchMethodException {
    // 檢查當前實現類是否實現了 type 接口
    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.");
    }
    // 當前實現類是否有 Adaptive 註解
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        cacheAdaptiveClass(clazz);
    // 當前類是否為 Wrapper 包裝擴展類
    } else if (isWrapperClass(clazz)) {
        cacheWrapperClass(clazz);
    } else {
        // 嘗試當前類是否有無參構造方法
        clazz.getConstructor();
        if (StringUtils.isEmpty(name)) {
            // 如果 name 為空,則獲取 clazz 的 @Extension 註解的值,如果註解值也沒有,則使用小寫類名
            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)) {
            // 緩存 擴展名和@Activate的緩存
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                // 緩存 擴展類和擴展名的緩存
                cacheName(clazz, n);
                // 將 擴展類和擴展名 保存到extensionClasses 擴展名->擴展類 關係映射中
                saveInExtensionClass(extensionClasses, clazz, n);

            }
        }
    }
}/<string>/<code>

至此,getExtensionClasses() 加載擴展類方法分析完成,接下分析注入依賴 injectExtension() 方法。

<code>private T injectExtension(T instance) {
    //
    if (objectFactory == null) {
        return instance;
    }
    try {
        for (Method method : instance.getClass().getMethods()) {
            // 遍歷當前擴展類的全部方法,如果當前方法不屬於 setter 方法,
            // 即不是以 'set'開頭的方法名,參數不是一個的,該方法訪問級別不是 public 的,則不往下執行
            if (!isSetter(method)) {
                continue;
            }
            // 當前方法是否添加了不要注入依賴的註解
            if (method.getAnnotation(DisableInject.class) != null) {
                continue;
            }
            Class> pt = method.getParameterTypes()[0];
            // 判斷當前參數是否屬於 八個基本類型或void
            if (ReflectUtils.isPrimitives(pt)) {
                continue;
            }
            try {
                // 通過屬性 setter 方法獲取屬性名
                String property = getSetterProperty(method);
                // 獲取依賴對象
                Object object = objectFactory.getExtension(pt, property);
                if (object != null) {
                    // 設置依賴
                    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;
}/<code>

通過遍歷擴展類所有方法,找到相對應的依賴,然後使用反射調用 settter 方法來進行設置依賴。 objectFactory 對象如圖:

Dubbo 擴展點加載機制:從 Java SPI 到 Dubbo SPI


其中找到相應依賴是在 SpiExtensionFactory 或 SpringExtensionFactory 中,同時,這兩個 Factory 保存在 AdaptiveExtensionFactory 中進行維護。

<code>@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {
    private final List<extensionfactory> factories;
    public AdaptiveExtensionFactory() {
        // ......
    }
    @Override
    public T getExtension(Class type, String name) {
        // 通過遍歷匹配到 type->name 的映射
        for (ExtensionFactory factory : factories) {
            T extension = factory.getExtension(type, name);
            if (extension != null) {
                return extension;
            }
        }
        return null;
    }
}
/<extensionfactory>/<code>

以上是對 Dubbo SPI 擴展類簡單加載過程分析完成。

自適應加載機制

為 Dubbo 更加靈活的使一個接口不通過硬編碼加載擴展機制,而是通過使用過程中進行加載,Dubbo 的另一加載機制——自適應加載。 自適應加載機制使用 @Adaptive 標註:

<code>@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
    String[] value() default {};
}/<code>

Adaptive 的值是一個數組,可以配置多個 key。初始化時,遍歷所有 key 進行匹配,如果沒有則匹配 @SPI 的值。 當 Adaptive 註解標註在類上時,則簡單對應該實現。如果註解標註在接口方法上時,則會根據參數動態生成代碼來獲取擴展點的實現。 類上註解處理還是比較好理解,方法上的註解加載相對比較有研讀性。通過調用ExtensionLoader#getAdaptiveExtension來進行獲取擴展實現。

<code>public T getAdaptiveExtension() {
    // 獲取實例化對象緩存
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
        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);
    }
}/<code>

上面代碼完成了擴展類對象是否存在緩存中,如果不存在,則通過創建自適應擴展,並將實例注入依賴後,設置在實例化後的自適應擴展對象中。 其中getAdaptiveExtensionClass是比較核心的流程。

<code>private Class> getAdaptiveExtensionClass() {
    // 加載全部擴展類
    getExtensionClasses();
    // 加載全部擴展類後,如果有 @Adaptive 標註的類,cachedAdaptiveClass 則一定不會為空
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    // 創建自適應擴展類
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
private Class> createAdaptiveExtensionClass() {
    // 生成自適應擴展代碼
    String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
    // 獲取擴展類加載器
    ClassLoader classLoader = findClassLoader();
    // 獲取編譯器類型的實現類
    org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    // 編譯代碼,返回該對象
    return compiler.compile(code, classLoader);
}/<code>

這裡完成的工作主要是,加載全部擴展類,代表所有擴展接口類的實現類,在其加載過程中,如果有 @Adaptive 標註的類,會保存到 cachedAdaptiveClass 中。通過自動生成自適應擴展代碼,並被編譯後,獲取擴展類實例化對象。 上面編譯器類型是可以指定的,通過 compiler 進行指定,例如:<applicationname>,該編譯器默認使用 javassist 編譯器。

在 generate 方法中動態生成代碼:

<code>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());
    // 生成類的依賴類
    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();
}/<code>

上面是生成類信息的方法,生成設計原理是按照已設置好的模板,進行替換操作,生成類。具體信息不代碼很多,但閱讀還是比較簡單。 自適應加載機制,已簡單分析完,咋一眼看,非常複雜,但是瞭解整體結構和流程,再去細研的話,相對還是好理解。

總結

從 Dubbo 設計來看,其良好的擴展性,比較重要的一點是得益於 Dubbo SPI 加載機制。在學習它的設計理念,對可擴展性方面的編碼思考也有一定的啟發。


分享到:


相關文章: