dubbo系列之dubbo SPI

Dubbo 版本2.7.0

爲什麼先講 SPI ? 因爲 Dubbo 的拓展實現就是採用這一種機制。

SPI 是一種服務發現機制,全稱爲 “Service Provider Interface”。SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣可以在運行時,動態爲接口替換實現類。Dubbo 則利用此特性爲程序提供拓展功能,不過,Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了增強,使其能夠更好的滿足需求。

所以,基於 SPI 機制,我們能夠很好的對 Dubbo 進行拓展。Dubbo SPI 源碼位於 org.apache.dubbo.common.extension 包下,後續會先介紹如何使用,再結合源碼詳細分析。

Dubbo 在考慮拓展點時,有一個設計概念叫做 “ 平等對待第三方 ”,也就是說框架作者能做到的功能,拓展者也一定能做到。微核心+插件式,則是比較能達到開閉原則的思路,詳細介紹參考官方文檔的開發者指南下的設計原則中的拓展點重構介紹。

以下示例參考自 dubbo 官方文檔。


Java SPI

首先,定義一個接口,名稱爲 Robot。

public interface Robot {

    void sayHello();
}

接下來,定義來個實現類:BumblebeeOptimusPrime

public class Bumblebee implements Robot {
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}

public class OptimusPrime implements Robot {
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Optimus Prime.");
    }
}

在不考慮 SPI 機制的情況下,要使用上述兩個實現類,則只能採用硬編碼的方式:通過構造函數創建對象,調用 sayHello 方法。如果現在有外部也想提供該接口的實現類供內部使用,這種就很難滿足了。

現在繼續考慮 SPI,在 META-INF/services 文件夾下創建一個與接口全限定名稱相同的文件,即 com.duofei.spi.Robot。文件內容則爲接口實現類的全限定名,如下:

com.duofei.spi.provider.Bumblebee
com.duofei.spi.provider.OptimusPrime

現在,編寫測試代碼:

	public void javaSPI(){
        ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
        serviceLoader.forEach(Robot::sayHello);
    }

最終結果會成功打印兩條輸出語句。現在如果,我們想在外部添加實現類供內部使用,那麼只需要在上述的文件中,新增內容爲接口實現類的全限定名稱即可。

儘管這種實現方式滿足了上述需求,但仍然會帶來一些問題,比如它會實例化所有的實現類,這樣就不太利於資源的利用了,當然,並不僅僅是由於以上原因,Dubbo 就實現了自己的一套 SPI ,畢竟量身定做,使用起來也會方便許多。


Dubbo SPI

Dubbo SPI 要求拓展點接口必須添加 @SPI 註解,即爲上述的 Robot 接口添加該註解。

關於拓展點接口實現類的描述文件,放在了 META-INF/dubbo 目錄下,並且內容改爲了鍵值對的形式,上述用例的文件內容爲:

bumblebee=com.duofei.spi.provider.Bumblebee
optimusPrime=com.duofei.spi.provider.OptimusPrime

文件名稱仍然使用接口的全限定名,下面是測試代碼:

    public void dubboSPI(){
        ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }

首先,按需加載可以從代碼中體現出來,但不僅僅如此,爲了讓拓展機制更加靈活好用,Dubbo 還加入了其它的一些特性,如注入拓展,自適應拓展機制,自動激活策略。


源碼分析:

Dubbo SPI 的整個加載機制差不多都在 ExtensionLoader 中了,那麼如何去閱讀源碼呢?除了上速的 getExtension 方法作爲入口之外,我一般喜歡按以下步驟去做分析:

  1. 查看類的所在的包結構,從一個更高的層次去看,反而,能體會到更多的東西;
  2. 查看類的繼承結構,從實現的接口和繼承的類,能夠感知類在整個框架中的角色;
  3. 查看類的構造函數,瞭解實例化該類,還需要哪些條件,從而牽引出一個鏈式的實現關係;
  4. 查看類的成員變量,俗話說: “巧婦難爲無米之炊”,你有了怎樣的數據,能夠在一定程度上表明你要做怎樣的事了。
  5. 查看類的結構圖,看類的私有、公有方法等,這種太籠統了,所以還是從類在使用所調用的方法去入手,就像上面所講的 getExtension

這種方式是個人的一些總結,當然,還有看文檔這是肯定的了。

該類並沒有實現任何接口或者繼承任何類,說明 SPI 的使用直接使用該類即可。該類的構造函數是私有的:

	private ExtensionLoader(Class<?> type) {
        this.type = type;
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }

這裏的 type 指的是拓展接口的 Class 對象。而 objectFactory 暫時看不明白,可以先擱置一邊。既然構造函數是私有的,那麼就一定有一個公共的靜態方法來獲取本身的一個對象實例,自然就找到了用例中使用的 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 interface!");
        }
        // type必須添加 SPI 註解
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type(" + type +
                    ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
        }
		// 嘗試從緩存中讀取
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

上述的邏輯是在獲取 ExtensionLoader 時,會先從緩存中去讀取,在不存在的情況,才重新情況下。EXTENSION_LOADERS 作爲成員變量,提供了緩存已經加載的 ExtensionLoader 實例,所以它會是一個靜態的,被所有實例所共享。

接着查看用例中的 getExtension(String) 方法:

	public T getExtension(String name) {
        if (name == null || name.length() == 0) {
            throw new IllegalArgumentException("Extension name == null");
        }
      	// 從這裏可以看出,拓展點描述文件的應該避免設置爲true
        if ("true".equals(name)) {
            return getDefaultExtension();
        }
        Holder<Object> holder = cachedInstances.get(name);
        if (holder == null) {
            cachedInstances.putIfAbsent(name, new Holder<Object>());
            holder = cachedInstances.get(name);
        }
        Object instance = holder.get();
        if (instance == null) {
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {
                    // 根據key嘗試創建實例,...接下來會展開介紹》
                    instance = createExtension(name);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }

上述實例的獲取也會嘗試先從緩存加載,緩存不存在的情況下,通過雙重檢測鎖纔去真正地實例化拓展點實現類。

雙重檢測外一層是爲了解決效率問題,避免大量線程競爭鎖,內一層纔是爲了真正解決併發所帶來的問題。

展開介紹 createExtension(name) 方法:

    private T createExtension(String name) {
        // 根據key獲取value的 class 對象,...接下來展開介紹》
        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);
            // 將拓展對象包裹在相應的 Wrapper對象中
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
                for (Class<?> wrapperClass : wrapperClasses) {
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                    type + ")  could not be instantiated: " + t.getMessage(), t);
        }
    }

getExtensionClasses() 方法將負責將指定的拓展點描述文件 key-value 鍵值對轉換爲 Map<String, Class<?>> ,存儲在實例的成員變量 cachedClasses 中,並且在該方法的調用鏈中還初始化了 cachedWrapperClasses 成員變量。該成員變量爲集合,用於將指定對象包裹在相應的 Wrapper 對象中,這在後續的 注入拓展 中會詳細描述。

將拓展對象包裹在相應的 Wrapper對象中,並將 Wrapper 對象返回,使用的場景是當前有拓展點 A,其接口實現中有 B、C、D,其中 B、C 的構造函數含有參數 A 類型,那麼 B、C 將作爲 Wrapper 對象,在獲取 D 時,最終返回的會是 C 的實現類,但 C 包裝了 B (即通過構造函數傳入),B 包裝了 D。 需要注意的是帶有構造函數含有參數 A 類型的 B、C 沒法在單獨創建,即調用 createExtension(name) 會拋出異常。


@SPI

該註解只有一個 value 屬性值,該值代表默認拓展名,該拓展名用於ExtensionLoader 實例對象的 getDefaultExtension 方法。


@Adaptive

該註解是自適應拓展機制,它的實現比較複雜,這裏只介紹其使用方式。

在 Robot 接口新增如下內容:

    @Adaptive("key")
    void showColor(URL url);

BumblebeeOptimusPrime 分別實現該方法如下:

	@Override
    public void showColor(URL url) {
        System.out.println("Hello, I an yellow.");
    }

	@Override
    public void showColor(URL url) {
        System.out.println("Hello, I am blue and white.");
    }

測試代碼如下:

		ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
        Robot robot = extensionLoader.getAdaptiveExtension();
        robot.showColor(URL.valueOf("dubbo://127.0.0.1:9092?key=bumblebee"));
        robot.showColor(URL.valueOf("dubbo://127.0.0.1:9092?key=optimusPrime"));

採用這種方式,能夠通過參數去決定使用哪個拓展點的實現類。


@Activate

該註釋對於根據給定的條件自動激活某些擴展非常有用。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {
    /**
     * 激活當前的 extension ,當 group 數組中的某個值得到匹配時
     */
    String[] group() default {};

    /**
     * 激活當前的 extension,當 URL 參數中,出現了 value 中聲明的 key 時
     */
    String[] value() default {};

    /**
     * 排序
     */
    int order() default 0;
}

用例:爲 OptimusPrime 實現類添加 @Activate(value = "key", group = "group") 註解,測試代碼如下:

		ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
        List<Robot> activateExtension =
                extensionLoader.getActivateExtension(URL.valueOf("dubbo://127.0.0.1:9092?key=optimusPrime"), "key", "group");
        activateExtension.forEach(Robot::sayHello);

儘管 getActivateExtension 提供了幾個重載方法,但最終實現都在 getActivateExtension(URL url, String[] values, String group) 中:

	public List<T> getActivateExtension(URL url, String[] values, String group) {
        List<T> exts = new ArrayList<T>();
        List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
        // keys 是否需要剔除默認激活的,即 values包含了 "-default" 
        if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
            getExtensionClasses();
            for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
                String name = entry.getKey();
                Object activate = entry.getValue();

                String[] activateGroup, activateValue;

                if (activate instanceof Activate) {
                    activateGroup = ((Activate) activate).group();
                    activateValue = ((Activate) activate).value();
                } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
                    activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
                    activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
                } else {
                    continue;
                }
                // 匹配組
                if (isMatchGroup(group, activateGroup)) {
                    T ext = getExtension(name);
                    // !names.contains(name) 的判斷,避免重複加載了 values 中指定的拓展名;
                    if (!names.contains(name)
                            && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
                            && isActive(activateValue, url)) {
                        exts.add(ext);
                    }
                }
            }
            Collections.sort(exts, ActivateComparator.COMPARATOR);
        }
        // 從拓展名稱中加載
        List<T> usrs = new ArrayList<T>();
        for (int i = 0; i < names.size(); i++) {
            String name = names.get(i);
            if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX)
                    && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) {
                if (Constants.DEFAULT_KEY.equals(name)) {
                    if (!usrs.isEmpty()) {
                        exts.addAll(0, usrs);
                        usrs.clear();
                    }
                } else {
                    T ext = getExtension(name);
                    usrs.add(ext);
                }
            }
        }
        if (!usrs.isEmpty()) {
            exts.addAll(usrs);
        }
        return exts;
    }

上面的獲取激活拓展涉及三個參數 ,分別是URL,values 拓展名,以及指定的組名;具體的匹配策略可按如下區分:

  • values 拓展名不包含剔除默認字符串("-default"):加載 Activate 註解的拓展類;

    • 指定組名匹配 Activate 註解的 group;
      • Activate 註解的value 值匹配 URL 中的key;
  • 加載 values 指定的拓展實現類

從以上大致可以判斷,dubbo 將帶有 Activate 註解的拓展當做 default ,我們可以在調用 getActivateExtension 方法時,在指定拓展點名稱時,在裏面包含 -default ,即可剔除默認的拓展實現;


依賴注入

在通過 createExtension 創建拓展點的時候,有這樣一個 injectExtension(instance) 方法調用,其目的是爲了注入拓展:

	private T injectExtension(T instance) {
        try {
            if (objectFactory != null) {
                // 反射獲取實例所有方法,遍歷方法列表
                for (Method method : instance.getClass().getMethods()) {
                    // 檢測方法名是否具有 setter 方法特徵
                    if (method.getName().startsWith("set")
                            && method.getParameterTypes().length == 1
                            && Modifier.isPublic(method.getModifiers())) {
                        /**
                         * 使用 DisableInject 註解,禁止依賴注入
                         */
                        if (method.getAnnotation(DisableInject.class) != null) {
                            continue;
                        }
                        Class<?> pt = method.getParameterTypes()[0];
                        if (ReflectUtils.isPrimitives(pt)) {
                            continue;
                        }
                        try {
                            String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                            Object object = objectFactory.getExtension(pt, property);
                            if (object != null) {
                                // 反射調用 setter 方法,將依賴設置到目標對象中
                                method.invoke(instance, object);
                            }
                        } catch (Exception e) {
                            logger.error("fail to inject via method " + method.getName()
                                    + " of interface " + type.getName() + ": " + e.getMessage(), e);
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return instance;
    }

這是 Dubbo IOC,其注入實現是將拓展實現類中帶有 setter 方法特徵的屬性注入。需要注入的依賴屬性來自 objectFactory 對象,查找該對象的實例化處,發現它是在構造函數中實例化的:

objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());

這裏已經在利用 SPI 的拓展特性,查看 ExtensionFactory 接口,其帶有 @SPI 註解,查看其實現類,分別是 AdaptiveExtensionFactorySpiExtensionFactorySpringExtensionFactoryAdaptiveExtensionFactory 用於創建自適應的拓展。SpiExtensionFactory則是用於從 dubbo SPI 中獲取所需要的拓展, SpringExtensionFactory是用於從 Spring 的 IOC 容器中獲取所需的拓展。

在上面代碼中,objectFactory 變量的類型爲 AdaptiveExtensionFactoryAdaptiveExtensionFactory內部維護了一個 ExtensionFactory 列表,用於存儲其他類型的 ExtensionFactory


總結

dubbo 自己實現了 SPI 機制,相比 Java 不靈活的加載方式,它大概提供了以下幾個功能:

  1. 由於拓展實現類描述文件內容改寫爲 key-value 的形式,所以可以實現按自定義key 獲取拓展實現類

  2. 由於要求拓展點必須添加 @SPI 註解,而該註解又支持默認拓展實現類的指定,所以可以實現指定默認拓展實現類

  3. 之前在創建 createExtension 中,有一個關於 cachedWrapperClasses 的操作,其實這是一種包裝,提供針對拓展點實現的一種包裝,有點類似於一種鏈。

  4. 關於 @Adaptive 註解,這個只有在通過 getAdaptiveExtension 獲取到的拓展時,纔會有效;並且限制了實現類中只有一個類能夠添加該註解;如果要將該註解添加在方法上,只能在拓展點的接口上添加

    在調用 getAdaptiveExtension 時遵循以下邏輯:

    1. 實現類中有一個類有該註解,返回該實現類,註解在拓展點方法上的 @Adaptive 不生效;
    2. 實現類中沒有該註解,拓展點接口上的某些方法存在該註解,那麼將通過代碼構建代理類的內容,並通過 "javassist" 編譯該對象生成 Class 對象(這裏也是利用了 SPI 的,具體可查看源碼),並在調用帶有 @Adaptive 方法時,再去選擇具體的實現類,如果調用了未註解的方法,則會得到一個異常;
    3. 實現類中沒有該註解,拓展點接口上的方法也沒有該註解,拋出異常;
  5. 關於 @Activate 註解,在調用 getActivateExtension 時生效;其屬性 values 針對方法參數 URL 中是否存在 key,屬性 group 針對方法參數 group,order 屬性則用於排序,並且添加了該註解的實現類會在調用該方法時,作爲 default 實現類,可以通過在方法參數中,添加 “-default” 排除。

其實對於各註解的使用不要混淆,它們只有在調用對應方法時,纔會生效;比如說你在調用 getExtension 方法時, @Activate 註解並不會生效。

可以說,對於 SPI 實現,dubbo 還是做了很多工作,重點關注 ExtensionLoadergetExtensiongetDefaultExtensiongetAdaptiveExtensiongetActivateExtension 幾個方法。


我與風來


認認真真學習,做思想的產出者,而不是文字的搬運工
錯誤之處,還望指出

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章