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

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

Java SPI

經過完成一個 Java SPI 的操做來了解它的機制。apache

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

Animal 接口數組

public interface AnimalService {
    void category();
}

Cat 實現類緩存

public class Cat implements AnimalService {

    @Override
    public void category() {
        System.out.println("cat: Meow ~");
    }
}

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

top.ytao.demo.spi.Cat

加載 SPI 的實現:app

public class JavaSPITest {

    @Test
    public void javaSPI() throws Exception {
        ServiceLoader<AnimalService> serviceLoader = ServiceLoader.load(AnimalService.class);
        // 遍歷在配置文件中已配置的 AnimalService 的全部實現類
        for (AnimalService animalService : serviceLoader) {
            animalService.category();
        }
    }

}

執行結果:ide

就這樣,一個 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 的配置文件內容:ui

cat = top.ytao.demo.spi.Cat

Dubbo SPI 加載過程當中,對 Java SPI 的目錄也是能夠被兼容的。

同時須要在接口上增長 @SPI 註解,@SPI 中能夠指定 key 值,加載 SPI 以下:

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();
    }

}

執行結果以下:

獲取 ExtensionLoader 實例

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

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> 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<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    if (loader == null) {
        // 加載 ExtensionLoader 實例到緩存中
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}

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

獲取擴展類對象

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

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;
}

獲取 holder 對象是從緩存ConcurrentMap<String, Holder<Object>> cachedInstances中獲取,若是不存在,則以擴展名 key,建立一個 Holder 對象做爲 value,設置到擴展對象緩存。 若是是新建立的擴展對象實例,那麼 holder.get() 必定是 null ,擴展對象爲空時,通過雙重檢查鎖,建立擴展對象。

建立擴展對象

建立擴展對象過程:

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);
    }
}

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

獲取全部擴展類:

private Map<String, Class<?>> getExtensionClasses() {
    // 獲取普通擴展類緩存
    Map<String, Class<?>> classes = cachedClasses.get();
    // 若是緩存中沒有,經過雙重檢查鎖後進行加載
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 加載所有擴展類
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

檢查普通擴展類緩存是否爲空,若是不爲空則從新加載,真正加載擴展類在loadExtensionClasses中:

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, Class<?>> loadExtensionClasses() {
    // 獲取 @SPI 上的默認擴展名
    cacheDefaultExtensionName();

    Map<String, Class<?>> 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;
}

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

private void loadDirectory(Map<String, Class<?>> 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);
        }
    }

這裏獲取文件名後加載全部同名文件,而後迭代各個文件,逐個加載文件內容。

private void loadResource(Map<String, Class<?>> 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);
    }
}

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

private void loadClass(Map<String, Class<?>> 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);
            }
        }
    }
}

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

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;
}

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

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

@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {

    private final List<ExtensionFactory> factories;

    public AdaptiveExtensionFactory() {
        // ......
    }

    @Override
    public <T> T getExtension(Class<T> type, String name) {
        // 經過遍歷匹配到 type->name 的映射
        for (ExtensionFactory factory : factories) {
            T extension = factory.getExtension(type, name);
            if (extension != null) {
                return extension;
            }
        }
        return null;
    }

}

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

自適應加載機制

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

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

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

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);
    }
}

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

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);
}

這裏完成的工做主要是,加載所有擴展類,表明全部擴展接口類的實現類,在其加載過程當中,若是有 @Adaptive 標註的類,會保存到 cachedAdaptiveClass 中。經過自動生成自適應擴展代碼,並被編譯後,獲取擴展類實例化對象。 上面編譯器類型是能夠指定的,經過 compiler 進行指定,例如:<dubbo:application name="taomall-provider" compiler="jdk" />,該編譯器默認使用 javassist 編譯器。

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

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

總結

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

推薦閱讀

Dubbo 路由機制的實現

Dubbo之服務消費原理

Dubbo之服務暴露

相關文章
相關標籤/搜索