SPI 全稱為 Service Provider Interface,是一種服務發現機制。當程式執行呼叫介面時,會根據配置檔案或預設規則資訊載入對應的實現類。所以在程式中並沒有直接指定使用介面的哪個實現,而是在外部進行裝配。
要想了解 Dubbo 的設計與實現,其中 Dubbo SPI 載入機制是必須瞭解的,在 Dubbo 中有大量功能的實現都是基於 Dubbo SPI 實現解耦,同時也使得 Dubbo 獲得如此好的可擴充套件性。

Java SPI

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

  • 建立一個 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 檔案中新增:

top.ytao.demo.spi.Cat

載入 SPI 的實現:

public class JavaSPITest {

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

}

執行結果:

就這樣,一個 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 如下:

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 載入機制。在學習它的設計理念,對可擴充套件性方面的編碼思考也有一定的啟發。

個人部落格: https://ytao.top
關注公眾號 【ytao】,更多原創好文
相關文章