【Dubbo 源碼解析】02_Dubbo SPI

Dubbo SPI:(version:2.6.*)

Dubbo 微內核 + 插件 模式,得益於 Dubbo SPI 。其中 ExtentionLoader是 Dubbo SPI 最核心的類,它負責擴展點的加載和生命週期管理。html

ExtensionLoader

ExtensionLoader 相似於 Java SPI 的 ServiceLoader,負責擴展的加載和生命週期維護,它是 Dubbo SPI 最核心的類。前端

使用最頻繁的 API 有以下幾個:java

  • public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type)apache

    -- 獲取 type 類對應的 ExtensionLoader緩存

  • public T getAdaptiveExtension()app

    -- 獲取擴展自適應實例(擴展適配器)。擴展點適配器的實現中,通常會調用下面的 API 來獲取指定的擴展點實例框架

    warn: dubbo 中大多數的擴展實例,都是經過擴展點匹配器 AdaptiveExtension 來獲取的函數

  • public T getExtension(String name)源碼分析

    -- 根據 name 獲取指定的擴展實例url

  • public T getDefaultExtension()

    -- 獲取 @SPI value 中指定的默認擴展點

  • public List<T> getActivateExtension(URL url, String key)

    -- 獲取被激活的擴展點集合

舉例:

  • Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); -- 獲取擴展點適配器。擴展點適配器實現類中會調用下面的代碼來獲取指定的擴展點實例

  • Protocol extension = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName); -- 獲取指定的擴展點

  • Protocol extension = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension(); -- 獲取 @SPI value 中指定的默認擴展點

  • ExtensionLoader.getExtensionLoader(ExporterListener.class).getActivateExtension(invoker.getUrl(), Constants.EXPORTER_LISTENER_KEY)) -- 獲取被激活的擴展點集合

 

Dubbo 擴展點機制基本概念

  • 擴展點(Extension Point)

    是一個Java的接口。

  • 擴展(Extension)

    擴展點的實現類。

  • 擴展實例(Extension Instance)

    擴展點實現類的實例。

  • 擴展自適應實例(Extension Adaptive Instance)

    擴展適配器。擴展適配器實例就是一個 Extension 的代理,它實現了擴展點接口。在調用擴展點的接口方法時,會根據實際的參數來決定要使用哪一個擴展。

  • @SPI

    @SPI 註解做用於擴展點的接口上,代表該接口是一個擴展點,能夠被 Dubbo 的ExtentionLoader 加載。若是沒有此註解的話, ExtensionLoader.getExtensionLoader(type) 調用會異常。

  • @Adaptive

    1. @Adaptive 註解使用在類上,表示這個類是一個擴展適配器。當調用 ExtensionLoader.getExtensionLoader(type).getAdaptiveExtension() 會獲取到該擴展適配器的實例。

    2. @Adaptive 註解使用在方法上,表示該方法是一個適配(自適應)方法。Dubbo 在爲擴展點生成擴展適配器時,若是方法上有 @Adaptive 註解,會爲該方法生成對應的實現。實現方法內部會根據方法的參數,來決定使用哪一個擴展。

 

舉例: 以 com.alibaba.dubbo.rpc.Protocol 爲例

@SPI("dubbo")
public interface Protocol {
    int getDefaultPort();
    
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
    
    void destroy();
}

 

Protocol 類上有 @SPI("dubbo"), 含有 4 個方法,其中有 2 個有 @Adaptive 註解。

  1. @SPI("dubbo")

    @SPI 表示 Protocol 是一個擴展點。若是 Protocol 類上沒有 @SPI 註解的話,試圖去加載擴展時,會拋出異常。

    "dubbo" 表示 Protocol 的默認擴展點的名稱是 "dubbo"

  2. @Adaptive

    這裏 @Adaptive 用在方法上,表示 export() 與 refer() 方法都是適配方法,Dubbo 會自動爲該方法生成適配實現。

    而 getDefaultPort() 和 destroy() 方法上沒有 @Adaptive 註解,則 Dubbo 在自動生成擴展適配器時,會讓這類方法拋出異常。

 

說明:

  • Dubbo 自動生成擴展實現的代碼參考 ExtensionLoader#createAdaptiveExtensionClassCode() 方法

    經過閱讀源碼,咱們能夠發現,Dubbo 是經過擴展點適配器在運行時動態選擇擴展點實現的,而動態選擇的策略是經過 URL 中的參數來決定的。

  • Duubo 爲 Protocol 生成的擴展點適配器(Protocol$Adaptive.class)的代碼以下:

package com.alibaba.dubbo.rpc;
​
import com.alibaba.dubbo.common.extension.ExtensionLoader;
​
public class Protocol$Adaptive implements com.alibaba.dubbo.rpc.Protocol {
    public void destroy() {
        throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }
​
    public int getDefaultPort() {
        throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }
​
    public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.RpcException {
        if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }
​
    public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws com.alibaba.dubbo.rpc.RpcException {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg1;
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }
}

 

 

 

源碼分析

根據代碼 ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension() 來分析 SPI 的加載過程:

#getExtensionLoader(Protocol.class)

  1. new 出一個 ExtensionLoader,並將 Protocol.class 賦值給成員變量 type

#getAdaptiveExtension() 獲取 SPI 擴展點適配器

#getExtensionClasses() 首先加載 type 類相關的全部擴展點實例

  1. 獲取 Protocol.class 上的 @SPI 註解,若是 value() 有且只有一個值,就將值存放到 cachedDefaultName 中;若是沒有值,就跳過。 cachedDefaultName 就是 @SPI 默認實現的擴展點 name

  2. 按目錄順序加載 SPI 擴展文件,擴展文件名爲:com.alibaba.dubbo.rpc.Protocol
    目錄順序爲:
    META-INF/dubbo/internal/
    META-INF/dubbo/
    META-INF/services/

  3. 依次讀取文件中的內容,內容格式有兩種:kv格式 和 v 格式
    dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
    com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
    k: 擴展點的 name (name 有多個值的話,用逗號","隔開)
    v: 擴展點對應的 class

    a. 若是 v 類上有 @Adaptive 註解,就將 v 賦值給 cachedAdaptiveClass
    b. 若是 v 類是裝飾類,那麼就將 v 添加到 cachedWrapperClasses 中 ​ 裝飾類:v 類含有一個參數的構造函數,且這個參數爲 SPI 接口 type,這裏爲 Protocol.class ​ clazz.getConstructor(type);
    c. 若是前端的狀況都不知足,且 v 有默認的構造函數 ​ 則檢查擴展點的 name 是否有值,沒有值的話就,取 v 上的 @Extension 註解的 value();沒有 @Extension 註解,就取 v 類的簡稱的小寫(simpleName - type.simpleName)
    獲取 v 類上的 @Activate 註解,有值的話就存放到 cachedActivates 裏面(cachedActivates.put(names[0], activate),@Activate 是用來激活指定的擴展點的)
    將 v 緩存到 cachedNames 中 (cachedNames.put(clazz, name))
    將 v 緩存到 cachedClasses 中 (extensionClasses.put(name, clazz))

#createAdaptiveExtensionClass()

  1. 通過上面的加載過程,若是 cachedAdaptiveClass 有值,就返回。沒有的話,就使用字節碼技術動態建立一個擴展點匹配器 AdaptiveExtensionClass 返回。(絕大部分的 SPI 擴展點匹配器 AdaptiveExtension 的建立方式)
    以 Protocol.class 爲例,動態建立出的擴展點適配器的類名爲 Protocol$Adaptive

  2. 將獲取到的擴展點匹配器實例 instance 存放到 cachedAdaptiveInstance 中並返回

#getExtension(name) 按名稱 name 去獲取擴展點

  1. 先按照上面 2-4 的步驟加載 SPI 擴展文件

  2. 從 cachedClasses 中獲取擴展類 v (getExtensionClasses().get(name))

  3. 經過反射建立擴展點實例 instance (clazz.newInstance()),並將實例放到緩存 EXTENSION_INSTANCES 中 (EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()))

  4. injectExtension() 注入擴展點實例 instance 依賴的擴展點 【IoC】
    a. 拿到 instance 類的 set 方法。set 方法須要是 public 且只有一個入參
    b. 經過 set 方法的名稱反推出擴展點的名字 name
    c. 經過 ExtensionFactory 獲取到 type + name 的擴展點(objectFactory.getExtension(pt, property),pt 爲 set 方法的入參 class 類型,property 爲擴展點的 name)
    d. 經過反射注入依賴的擴展點(method.invoke(instance, object))

  5. 將全部的 cachedWrapperClasses 包裝在 instance 上,並按照 4 的流程注入 wrapperClass 實例依賴的擴展點 【AOP】

 

附:

  • 獲取 SPI 擴展點有兩種方式:

  1. 先獲取擴展點適配器,而後在方法調用時,擴展點適配器會動態路由到指定的擴展點去調用

  2. 經過 getExtension(name) 或者 getDefaultExtension() 方法直接獲取 以 Protocol.class 爲例,dubbo 在暴露一個服務的時候,會調用 Protocol#export(Invoker<T> invoker) 方法。
    具體它是先獲取擴展點適配器 Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(),而後再經過擴展點適配器去調用 export(Invoker<T> invoker) 方法。
    而擴展點適配器 Protocol$Adaptive.class 的 export(Invoker<T> invoker) 方法則是根據 invoker 中指定的擴展點名稱 extName 去調用指定擴展點的 export(Invoker<T> invoker) 方法

  • #getActivateExtension(URL url, String key, 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);
    // 添加框架默認激活的擴展點
    if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
        getExtensionClasses();
        for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
            String name = entry.getKey();
            Activate activate = entry.getValue();
            if (isMatchGroup(group, activate.group())) {
                T ext = getExtension(name);
                if (!names.contains(name) /* 添加默認激活的擴展點,須要排除指定激活的 */
                        && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
                        && isActive(activate, url)) {
                    exts.add(ext);
                }
            }
        }
        Collections.sort(exts, ActivateComparator.COMPARATOR);
    }
    
    // 添加 url 中指定激活的擴展點
    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;
}

 

 

參考:

@see: http://dubbo.apache.org/zh-cn/blog/introduction-to-dubbo-spi.html

@see: http://dubbo.apache.org/zh-cn/blog/introduction-to-dubbo-spi-2.html

相關文章
相關標籤/搜索