天天看點

Dubbo SPI機制核心原理,你掌握了嗎?

作者:田維常

這篇文章内容很幹,做好心理準備!本文詳細講解了 Dubbo SPI 誕生原因以及它的用法,并且詳細解讀了核心類 ExtensionLoader 的關鍵屬性,再根據demo 對 SPI 的加載原理進行詳細解讀。

文章較長,建議收藏!文末有原理圖。

為什麼不使用 JDK SPI

我們接着上次的文章講,既然已經有了 JDK SPI 為什麼還需要 Dubbo SPI 呢?

技術的出現通常都是為了解決現有問題,通過之前的 demo,不難發現 JDK SPI 機制就存在以下一些問題:

  1. 實作類會被全部周遊并且執行個體化,假如我們隻需要使用其中的一個實作,這在實作類很多的情況下無疑是對機器資源巨大的浪費,
  2. 無法按需擷取實作類,不夠靈活,我們需要周遊一遍所有實作類才能找到指定實作。

Dubbo SPI 以 JDK SPI 為參考做出了改進設計,進行了性能優化以及功能增強,Dubbo SPI 機制的出現解決了上述問題。除此之外,Dubbo 的 SPI 還支援自适應擴充以及 IOC 和 AOP 等進階特性。

JDK SPI 原理,請移步這篇文章:

Dubbo SPI機制核心原理,你掌握了嗎?

提到 Dubbo,為什麼一定要談SPI?|原創

Dubbo SPI配置

Dubbo SPI 的配置做出了改進,在 Dubbo 中有三種不同的目錄可以存放 SPI 配置,用途也不同。

  • META-INF/services/ 目錄:此目錄配置檔案用于相容 JDK SPI 。
  • META-INF/dubbo/ 目錄:此目錄用于存放使用者自定義 SPI 配置檔案。
  • META-INF/dubbo/internal/ 目錄:此目錄用于存放 Dubbo 内部使用的 SPI 配置檔案。

并且配置檔案中可以配置成Key-Value 形式,key 為擴充名,value 為具體實作類。有了擴充名就可以動态根據名字來定位到具體的實作類,并且可以針對性的執行個體化需要的實作類。比如指定 zookeeper,就知道要使用 zookeeper 作為注冊中心的實作。

zookeeper=org.apache.dubbo.registry.zookeeper.ZookeeperRegistryFactory           

Dubbo 并未使用 Java SPI,而是重新實作了一套功能更強的 SPI 機制。Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以加載指定的實作類。

除了對應目錄的配置,還需要 @SPI 注解的配合,被修飾的接口表示這是一個擴充接口,@SPI 的 value 表示這個擴充接口的預設擴充實作的擴充名,擴充名就是配置檔案中對應實作類配置資訊的 key。

//dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
@SPI("dubbo")
public interface Protocol {
}
           

除此之外,還需要 @Adaptive注解配合,被他修飾的類會生成Dubbo的擴充卡,Adaptive注解比較複雜,關于這個注解我們後續文章會再詳細講解。

ExtensionLoader

之前的文章我們提到過,dubbo-common 子產品中的 extension 包包含了 Dubbo SPI 的邏輯,而ExtensionLoader 就位于其中,Dubbo SPI 的核心邏輯幾乎都封裝在 ExtensionLoader 這個類裡,其地位你可以類比為 JDK SPI 中的 java.util.ServiceLoader。

學習 ExtensionLoader 首先了解以下幾個關鍵屬性。

核心靜态變量

1、EXTENSION_LOADERS

// 擴充接口和ExtensionLoader的關系
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);
           

這個字段儲存了擴充接口和ExtensionLoader之間的映射關系,一個擴充接口對應一個ExtensionLoader。key 為接口,value 為 ExtensionLoader。

Dubbo SPI機制核心原理,你掌握了嗎?

2、EXTENSION_INSTANCES

//擴充接口實作類和執行個體對象的關系
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>(64);
           

這個字段儲存了擴充接口擴充實作類和執行個體對象之間的關系,key 為擴充實作類,value為執行個體對象。

3、strategies 加載政策

//加載政策
private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();
import static java.util.ServiceLoader.load;
private static LoadingStrategy[] loadLoadingStrategies() {
    return stream(load(LoadingStrategy.class).spliterator(), false)
           .sorted().toArray(LoadingStrategy[]::new);
}
           

strategies 對應的是Dubbo内部實作的三個加載政策,分别對應之前提到的三個不同的SPI配置目錄,接口的繼承關系如下。他們的實作中無非就是包含了兩個資訊:加載目錄和優先級。

Dubbo SPI機制核心原理,你掌握了嗎?

每個實作類都繼承了優先級接口Prioritized,是以不同加載政策的加載優先級不同,對應的優先級是:

DubboInternalLoadingStrategy 大于>DubboLoadingStrategy 大于>ServicesLoadingStrateg

對應目錄的優先級分别是:

META-INF/dubbo/internal/>META-INF/dubbo/>META-INF/services/

LoadingStrategy是如何加載的?

需要注意的是,LoadingStrategy的控制了Dubbo内部實作的加載政策,那他自身又是如何加載的呢?

其實根據上面的代碼就可以發現LoadingStrategy正是依賴 JDK SPI 機制來加載的,在loadLoadingStrategies()方法中調用了ServiceLoader.load()方法,從META-INF/services檔案夾下的org.apache.dubbo.common.extension.LoadingStrategy檔案中讀取到了具體實作類并且依次加載。是以一定程度上來說,Dubbo SPI 機制正是依賴于 JDK SPI 機制。

Dubbo SPI機制核心原理,你掌握了嗎?

核心成員變量

1、type:與目前 ExtensionLoader 綁定的擴充接口類型。

2、cachedNames:存儲了擴充實作類和擴充名的關系,key 為擴充實作類,value 為對應擴充名。

3、cachedClasses:存儲了擴充名和擴充實作類之間的關系。key 為擴充名,value 為擴充實作類,這個 map 可以和 cachedNames 對應着了解,他們存貯内容是相反的關系,算是一種空間換時間的技巧。

4、cachedInstances:存儲了擴充實作類類名與實作類的執行個體對象之間的關系,key 為擴充實作名,value 為執行個體對象。

5、cachedDefaultName:@SPI 注解配置的預設擴充名。

//擴充接口類型
private final Class<?> type;
//存儲了擴充實作類和擴充名的關系
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
//存儲了擴充名和擴充實作類之間的關系
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
//存儲了擴充實作類類名與執行個體對象之間的關系
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
//擴充接口對@SPI注解配置的value
private String cachedDefaultName;
           

Dubbo SPI 加載原理

測試 demo

為了友善調試并且抓住核心原理,其實可以跟 JDK SPI 一樣寫一個 demo,我們直接複用上次的擴充接口和實作類,但是這次是用Dubbo SPI 的形式加載。配置檔案需要重新寫一份,寫成key-value形式,并且放在META-INF/dubbo檔案夾下。

/**
 * @author 後端開發技術
 */
@SPI
public interface MySPI {
    void say();
}
public class HelloMySPI implements MySPI{
    @Override
    public void say() {
        System.out.println("HelloMySPI say:hello");
    }
}
public class GoodbyeMySPI implements MySPI {
    @Override
    public void say() {
        System.out.println("GoodbyeMySPI say:Goodbye");
    }
}
public static void main(String[] args) {
    ExtensionLoader<MySPI> extensionLoader = ExtensionLoader.getExtensionLoader(MySPI.class);
    MySPI hello = extensionLoader.getExtension("hello");
    hello.say();
    MySPI goodbye = extensionLoader.getExtension("goodbye");
    goodbye.say();
}
//配置檔案 META-INF/dubbo/org.daley.spi.demo.MySPI
goodbye=org.daley.spi.demo.GoodbyeMySPI
hello=org.daley.spi.demo.HelloMySPI
  
// 輸出結果
HelloMySPI say:hello
GoodbyeMySPI say:Goodbye
           

一定記得給接口标記@SPI注解,否則會抛出以下錯誤。

Exception in thread "main" java.lang.IllegalArgumentException: Extension type (interface org.daley.spi.demo.MySPI) is not an extension, because it is NOT annotated with @SPI!
           

獲得擴充執行個體

在上述 demo 中,我們首先通過 ExtensionLoader 的 getExtensionLoader 方法擷取一個 ExtensionLoader 執行個體,然後再通過 ExtensionLoader 的 getExtension 方法擷取拓展類對象。

在調用getExtensionLoader()時會首先嘗試從EXTENSION_LOADERS中根據擴充點類型獲得ExtensionLoader,如果未命中緩存,則會新建立一個ExtensionLoader,然後傳回。

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
   ……省略校驗邏輯
    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的構造方法中,會綁定擴充點接口類型,并且會綁定擴充對象工廠ExtensionFactory objectFactory,這也是一個擴充點,為了避免越套越深,這裡不做深入追蹤。

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

建立好ExtensionLoader之後,便可以調用getExtension()方法。核心在于獲得擴充實作,首先檢查緩存,緩存未命中則建立擴充對象,然後設定到 holder 中。

public T getExtension(String name) {
    return getExtension(name, true);
}

public T getExtension(String name, boolean wrap) {
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    if ("true".equals(name)) {
        // 擷取預設的擴充實作類
        return getDefaultExtension();
    }
    //用于持有目标對象
    final Holder<Object> holder = getOrCreateHolder(name);
    Object instance = holder.get();
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                //建立擴充執行個體
                instance = createExtension(name, wrap);
                //設定到holder中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}
           

建立擴充執行個體方法createExtension()主要包含以下邏輯:

  1. 通過 getExtensionClasses() 擷取所有的拓展類
  2. 通過反射建立拓展對象
  3. 向拓展對象中注入依賴(可以想象到,這裡存在依賴其他SPI擴充點的情況,遞歸注入)
  4. 将拓展對象包裹在相應的 Wrapper 對象中
  5. 如果實作了Lifecycle接口,執行生命周期的初始化方法

步驟 1 是加載拓展類的關鍵,步驟 3 和 4 是 Dubbo IOC 與 AOP 的具體實作,注意 IOC 和 AOP 是一種思想,别和 Spring 的 IOC、AOP 混淆。

private T createExtension(String name, boolean wrap) {
    //關鍵代碼!從配置檔案中加載所有的拓展類,可得到“配置項名稱”到“配置類”的映射關系表
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null || unacceptableExceptions.contains(name)) {
        throw findException(name);
    }
    try {
        // 通過反射建立執行個體
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // 向執行個體中注入依賴
        injectExtension(instance);


        if (wrap) {

            List<Class<?>> wrapperClassesList = new ArrayList<>();
            if (cachedWrapperClasses != null) {
                wrapperClassesList.addAll(cachedWrapperClasses);
                wrapperClassesList.sort(WrapperComparator.COMPARATOR);
                Collections.reverse(wrapperClassesList);
            }

            if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
                // 循環建立 Wrapper 執行個體
                for (Class<?> wrapperClass : wrapperClassesList) {
                    Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
                    if (wrapper == null
                            || (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
                        // 将目前 instance 作為參數傳給 Wrapper 的構造方法,并通過反射建立 Wrapper 執行個體。
                        // 然後向 Wrapper 執行個體中注入依賴,最後将 Wrapper 執行個體再次指派給 instance 變量
                        instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                    }
                }
            }
        }
    //執行生命周期初始化方法 initialize()
        initExtension(instance);
        return instance;
    } catch (Throwable t) {
        ……
    }
}           

獲得所有擴充類

我們在根據名稱獲得指定拓展類之前,首先需要根據配置檔案解析出拓展項名稱到拓展類的映射Map(Map<名稱, 拓展類>),之後再根據拓展項名稱從映射關系 Map 中根據 name 取出相應的拓展類即可。

在獲得全部擴充名與擴充類關系classes時,如果還未建立,會有個雙重檢查的過程以防止并發加載。通過檢查後會調用 loadExtensionClasses() 加載擴充類。相關過程的代碼分析如下:

private Map<String, Class<?>> getExtensionClasses() {
    // 從緩存中擷取已加載的拓展類
    Map<String, Class<?>> classes = cachedClasses.get();
    // DBC 雙重檢查
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 加載所有拓展類邏輯
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}
           

loadExtensionClasses()主要做了兩件事,一是解析@SPI注解設定預設擴充名,二是在這裡會掃描 Dubbo 那三個配置檔案夾下的所有配置檔案,具體注釋如下。

private Map<String, Class<?>> loadExtensionClasses() {
    cacheDefaultExtensionName();

    Map<String, Class<?>> extensionClasses = new HashMap<>();
    // 從三種加載政策中獲得擴充實作類,對應加載指定檔案夾下的配置檔案,這裡的加載政策已經根據優先級排好了順序,數字越小越優先
    for (LoadingStrategy strategy : strategies) {
        loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(),
                strategy.overridden(), strategy.excludedPackages());
        // 此處為了相容阿裡巴巴到Apache的版本過度
        loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"),
                strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
    }

    return extensionClasses;
}

// 設定預設擴充名
private void cacheDefaultExtensionName() {
    // 擷取 SPI 注解,這裡的 type 變量是在調用 getExtensionLoader 方法時傳入的
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation == null) {
        return;
    }
    //從@SPI注解獲得預設實作擴充實作名
    String value = defaultAnnotation.value();
    if ((value = value.trim()).length() > 0) {
        // 對 SPI 注解内容進行切分
        String[] names = NAME_SEPARATOR.split(value);
        // 檢測 SPI 注解内容是否合法,不合法則抛出異常
        if (names.length > 1) {
            throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
                    + ": " + Arrays.toString(names));
        }
        if (names.length == 1) {
            // 設定預設名稱,參考 getDefaultExtension 方法
            cachedDefaultName = names[0];
        }
    }
}
           

可以看出,關鍵的邏輯應該在loadDirectory()方法中。loadDirectory() 方法先通過 classLoader 擷取所有資源連結,然後再通過 loadResource 方法加載資源。我們繼續跟下去,看一下 loadResource 方法的實作。

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
                          java.net.URL resourceURL, boolean overridden, String... excludedPackages) {
    try {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
            String line;
            String clazz = null;
            // 按行讀取配置内容
            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('=');
                        if (i > 0) {
                            name = line.substring(0, i).trim();
                            clazz = line.substring(i + 1).trim();
                        } else {
                            clazz = line;
                        }
                        if (StringUtils.isNotEmpty(clazz) && !isExcluded(clazz, excludedPackages)) {
                            // 加載類,并通過 loadClass 方法對類進行緩存
                            loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);
                        }
                    } 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);
    }
}
           

loadResource 方法用于讀取和解析配置檔案,并通過反射加載類,最後調用 loadClass 方法進行其他操作。loadClass 方法用于主要用于操作緩存,該方法的邏輯如下:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
                       boolean overridden) throws NoSuchMethodException {
    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)) {
        // 設定 cachedAdaptiveClass緩存
        cacheAdaptiveClass(clazz, overridden);

        // 檢測 clazz 是否是 Wrapper 類型
    } else if (isWrapperClass(clazz)) {
        // 存儲 clazz 到 cachedWrapperClasses 緩存中
        cacheWrapperClass(clazz);

    } else {
        // 程式進入此分支,表明 clazz 是一個普通的拓展類
        clazz.getConstructor();
        // 檢測 clazz 是否有預設的構造方法,如果沒有,則抛出異常
        if (StringUtils.isEmpty(name)) {
            // 如果 name 為空,則嘗試從 Extension 注解中擷取 name,或使用小寫的類名作為 name
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException(
                        "No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
        }
        // 切分 name
        String[] names = NAME_SEPARATOR.split(name);
        if (ArrayUtils.isNotEmpty(names)) {
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                // 存儲 Class 到名稱的映射關系
                cacheName(clazz, n);
                // 存儲名稱到 Class 的映射關系
                saveInExtensionClass(extensionClasses, clazz, n, overridden);
            }
        }
    }
}
           

如上,loadClass 方法操作了不同的緩存,比如解析@Adaptive注解設定到cachedAdaptiveClass緩存;如果是wrapper類設定到cachedWrapperClasses 緩存;儲存拓展類和拓展名到 cachedNames 緩存等等。

到這裡 Dubbo SPI 加載機制的主要邏輯就講完了,彙總了一下核心流程如下圖:

Dubbo SPI機制核心原理,你掌握了嗎?

總結

在我們了解了各種實作細節後,最後可以總結一下各種知識點。

  1. dubbo 的 ExtensionLoader 對應 JDK 的ServiceLoader,包含了加載所需要的各種上下文資訊,一個擴充接口對應一個 ExtensionLoader,關系維護在一個 Map 中。
  2. 在嘗試擷取拓展類的時候,才開始執行個體化,按需加載,這種懶加載是一種對 JDK SPI 的優化。
  3. Dubbo SPI 會從三個配置目錄按照優先級加載配置,加載政策是依賴 JDK SPI 加載的。
  4. 加載配置的時候會加載所有配置的Class類型,然後把拓展名和拓展類的類型維護一個映射(key-value),這也是按需加載,靈活使用的基礎。

如果覺得對你有幫助,歡迎點贊、标或分享!

原文作者:後端開發技術

繼續閱讀