Dubbo 源碼分析 - SPI 機制

1.簡介

SPI 全稱爲 Service Provider Interface,是 Java 提供的一種服務發現機制。SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣能夠在運行時,動態爲接口替換實現類。正所以特性,咱們能夠很容易的經過 SPI 機制爲咱們的程序提供拓展功能。SPI 機制在第三方框架中也有所應用,好比 Dubbo 就是經過 SPI 機制加載全部的組件。不過,Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了加強,使其可以更好的知足需求。在 Dubbo 中,SPI 是一個很是重要的模塊。若是你們想要學習 Dubbo 的源碼,SPI 機制務必弄懂。下面,咱們先來了解一下 Java SPI 與 Dubbo SPI 的使用方法,而後再來分析 Dubbo SPI 的源碼。java

2.SPI 示例

2.1 Java SPI 示例

前面簡單介紹了 SPI 機制的原理,本節經過一個示例來演示 JAVA SPI 的使用方法。首先,咱們定義一個接口,名稱爲 Robot。數組

public interface Robot {
    void sayHello();
}

接下來定義兩個實現類,分別爲擎天柱 OptimusPrime 和大黃蜂 Bumblebee。緩存

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

public class Bumblebee implements Robot {

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

接下來 META-INF/services 文件夾下建立一個文件,名稱爲 Robot 的全限定名 com.tianxiaobo.spi.Robot。文件內容爲實現類的全限定的類名,以下:ruby

com.tianxiaobo.spi.OptimusPrime
com.tianxiaobo.spi.Bumblebee

作好了所需的準備工做,接下來編寫代碼進行測試。app

public class JavaSPITest {

    @Test
    public void sayHello() throws Exception {
        ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
        System.out.println("Java SPI");
        serviceLoader.forEach(Robot::sayHello);
    }
}

最後來看一下測試結果,以下:框架

從測試結果能夠看出,咱們的兩個實現類被成功的加載,並輸出了相應的內容。關於 Java SPI 的演示先到這,接下來演示 Dubbo SPI。ide

2.2 Dubbo SPI 示例

Dubbo 並未使用 Java SPI,而是從新實現了一套功能更強的 SPI 機制。Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,經過 ExtensionLoader,咱們能夠加載指定的實現類。Dubbo SPI 的實現類配置放置在 META-INF/dubbo 路徑下,下面來看一下配置內容。源碼分析

optimusPrime = com.tianxiaobo.spi.OptimusPrime
bumblebee = com.tianxiaobo.spi.Bumblebee

與 Java SPI 實現類配置不一樣,Dubbo SPI 是經過鍵值對的方式進行配置,這樣咱們就能夠按需加載指定的實現類了。另外,在測試 Dubbo SPI 時,須要在 Robot 接口上標註 @SPI 註解。下面來演示一下 Dubbo SPI 的使用方式:學習

public class DubboSPITest {

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

測試結果以下:測試

演示完 Dubbo SPI,下面來看看 Dubbo SPI 對 Java SPI 作了哪些改進,如下內容引用至 Dubbo 官方文檔。

  • JDK 標準的 SPI 會一次性實例化擴展點全部實現,若是有擴展實現初始化很耗時,但若是沒用上也加載,會很浪費資源。
  • 若是擴展點加載失敗,連擴展點的名稱都拿不到了。好比:JDK 標準的 ScriptEngine,經過 getName() 獲取腳本類型的名稱,但若是 RubyScriptEngine 由於所依賴的 jruby.jar 不存在,致使 RubyScriptEngine 類加載失敗,這個失敗緣由被吃掉了,和 ruby 對應不起來,當用戶執行 ruby 腳本時,會報不支持 ruby,而不是真正失敗的緣由。
  • 增長了對擴展點 IOC 和 AOP 的支持,一個擴展點能夠直接 setter 注入其它擴展點。

在以上改進項中,第一個改進項比較好理解。第二個改進項沒有進行驗證,就很少說了。第三個改進項是增長了對 IOC 和 AOP 的支持,這是什麼意思呢?這裏簡單解釋一下,Dubbo SPI 加載完拓展實例後,會經過該實例的 setter 方法解析出實例依賴項的名稱。好比經過 setProtocol 方法名,可知道目標實例依賴 Protocal。知道了具體的依賴,接下來便可到 IOC 容器中尋找或生成一個依賴對象,並經過 setter 方法將依賴注入到目標實例中。說完 Dubbo IOC,接下來講說 Dubbo AOP。Dubbo AOP 是指使用 Wrapper 類(可自定義實現)對拓展對象進行包裝,Wrapper 類中包含了一些自定義邏輯,這些邏輯可在目標方法前行先後被執行,相似 AOP。Dubbo AOP 實現的很簡單,其實就是個代理模式。這個官方文檔中有所說明,你們有興趣能夠查閱一下。

關於 Dubbo SPI 的演示,以及與 Java SPI 的對比就先這麼多,接下來加入源碼分析階段。

3. Dubbo SPI 源碼分析

上一章,我簡單演示了 Dubbo SPI 的使用方法。咱們首先經過 ExtensionLoader 的 getExtensionLoader 方法獲取一個 ExtensionLoader 實例,而後再經過 ExtensionLoader 的 getExtension 方法獲取拓展類對象。這其中,getExtensionLoader 用於從緩存中獲取與拓展類對應的 ExtensionLoader,若緩存未命中,則建立一個新的實例。該方法的邏輯比較簡單,本章就不就行分析了。下面咱們從 ExtensionLoader 的 getExtension 方法做爲入口,對拓展類對象的獲取過程進行詳細的分析。

public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    if ("true".equals(name)) {
        // 獲取默認的拓展實現類
        return getDefaultExtension();
    }
    // Holder 僅用於持有目標對象,沒其餘什麼邏輯
    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) {
                // 建立拓展實例,並設置到 holder 中
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

上面代碼的邏輯比較簡單,首先檢查緩存,緩存未命中則建立拓展對象。下面咱們來看一下建立拓展對象的過程是怎樣的。

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 (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            // 循環建立 Wrapper 實例
            for (Class<?> wrapperClass : wrapperClasses) {
                // 將當前 instance 做爲參數建立 Wrapper 實例,而後向 Wrapper 實例中注入屬性值,
                // 並將 Wrapper 實例賦值給 instance
                instance = injectExtension(
                    (T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("...");
    }
}

createExtension 方法的邏輯稍複雜一下,包含了以下的步驟:

  1. 經過 getExtensionClasses 獲取全部的拓展類
  2. 經過反射建立拓展對象
  3. 向拓展對象中注入依賴
  4. 將拓展對象包裹在相應的 Wrapper 對象中

以上步驟中,第一個步驟是加載拓展類的關鍵,第三和第四個步驟是 Dubbo IOC 與 AOP 的具體實現。在接下來的章節中,我將會重點分析 getExtensionClasses 方法的邏輯,以及簡單分析 Dubbo IOC 的具體實現。

3.1 獲取全部的拓展類

咱們在經過名稱獲取拓展類以前,首先須要根據配置文件解析出名稱到拓展類的映射,也就是 Map<名稱, 拓展類>。以後再從 Map 中取出相應的拓展類便可。相關過程的代碼分析以下:

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

這裏也是先檢查緩存,若緩存未命中,則經過 synchronized 加鎖。加鎖後再次檢查緩存,並判空。此時若是 classes 仍爲 null,則加載拓展類。以上代碼的寫法是典型的雙重檢查鎖,前面所分析的 getExtension 方法中有類似的代碼。關於雙重檢查就說這麼多,下面分析 loadExtensionClasses 方法的邏輯。

private Map<String, Class<?>> loadExtensionClasses() {
    // 獲取 SPI 註解,這裏的 type 是在調用 getExtensionLoader 方法時傳入的
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
        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("...");
            }

            // 設置默認名稱,cachedDefaultName 用於加載默認實現,參考 getDefaultExtension 方法
            if (names.length == 1) {
                cachedDefaultName = names[0];
            }
        }
    }

    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    // 加載指定文件夾配置文件
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    loadDirectory(extensionClasses, DUBBO_DIRECTORY);
    loadDirectory(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}

loadExtensionClasses 方法總共作了兩件事情,一是對 SPI 註解進行解析,二是調用 loadDirectory 方法加載指定文件夾配置文件。SPI 註解解析過程比較簡單,無需多說。下面咱們來看一下 loadDirectory 作了哪些事情。

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
    // fileName = 文件夾路徑 + type 全限定名 
    String fileName = dir + type.getName();
    try {
        Enumeration<java.net.URL> urls;
        ClassLoader classLoader = findClassLoader();
        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("...");
    }
}

loadDirectory 方法代碼很少,理解起來不難。該方法先經過 classLoader 獲取全部資源連接,而後再經過 loadResource 方法加載資源。咱們繼續跟下去,看一下 loadResource 方法的實現。

private void loadResource(Map<String, Class<?>> extensionClasses, 
    ClassLoader classLoader, java.net.URL resourceURL) {
    try {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(resourceURL.openStream(), "utf-8"));
        try {
            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('=');
                        if (i > 0) {
                            // 以 = 爲界,截取鍵與值。好比 dubbo=com.alibaba....DubboProtocol
                            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("...");
                    }
                }
            }
        } finally {
            reader.close();
        }
    } catch (Throwable t) {
        logger.error("...");
    }
}

loadResource 方法用於讀取和解析配置文件,並經過反射加載類,最後調用 loadClass 方法進行其餘操做。loadClass 方法有點名存實亡,它的功能只是操做緩存,而非加載類。該方法的邏輯以下:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, 
    Class<?> clazz, String name) throws NoSuchMethodException {
    
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("...");
    }

    if (clazz.isAnnotationPresent(Adaptive.class)) {    // 檢測目標類上是否有 Adaptive 註解
        if (cachedAdaptiveClass == null) {
            // 設置 cachedAdaptiveClass緩存
            cachedAdaptiveClass = clazz;
        } else if (!cachedAdaptiveClass.equals(clazz)) {
            throw new IllegalStateException("...");
        }
    } else if (isWrapperClass(clazz)) {    // 檢測 clazz 是不是 Wrapper 類型
        Set<Class<?>> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            wrappers = cachedWrapperClasses;
        }
        // 存儲 clazz 到 cachedWrapperClasses 緩存中
        wrappers.add(clazz);
    } else {    // 程序進入此分支,代表是一個普通的拓展類
        // 檢測 clazz 是否有默認的構造方法,若是沒有,則拋出異常
        clazz.getConstructor();
        if (name == null || name.length() == 0) {
            // 若是 name 爲空,則嘗試從 Extension 註解獲取 name,或使用小寫的類名做爲 name
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("...");
            }
        }
        // 切分 name
        String[] names = NAME_SEPARATOR.split(name);
        if (names != null && names.length > 0) {
            Activate activate = clazz.getAnnotation(Activate.class);
            if (activate != null) {
                // 若是類上有 Activate 註解,則使用 names 數組的第一個元素做爲鍵,
                // 存儲 name 到 Activate 註解對象的映射關係
                cachedActivates.put(names[0], activate);
            }
            for (String n : names) {
                if (!cachedNames.containsKey(clazz)) {
                    // 存儲 Class 到名稱的映射關係
                    cachedNames.put(clazz, n);
                }
                Class<?> c = extensionClasses.get(n);
                if (c == null) {
                    // 存儲名稱到 Class 的映射關係
                    extensionClasses.put(n, clazz);
                } else if (c != clazz) {
                    throw new IllegalStateException("...");
                }
            }
        }
    }
}

如上,loadClass 方法操做了不一樣的緩存,好比 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等。除此以外,該方法沒有其餘什麼邏輯了,就很少說了。

到此,關於緩存類加載的過程就分析完了。整個過程沒什麼特別複雜的地方,你們循序漸進的分析就好了,不懂的地方能夠調試一下。接下來,咱們來聊聊 Dubbo IOC 方面的內容。

3.2 Dubbo IOC

Dubbo IOC 是基於 setter 方法注入依賴。Dubbo 首先會經過反射獲取到實例的全部方法,而後再遍歷方法列表,檢測方法名是否具備 setter 方法特徵。如有,則經過 ObjectFactory 獲取依賴對象,最後經過反射調用 setter 方法將依賴設置到目標對象中。整個過程對應的代碼以下:

private T injectExtension(T instance) {
    try {
        if (objectFactory != null) {
            // 遍歷目標類的全部方法
            for (Method method : instance.getClass().getMethods()) {
                // 檢測方法是否以 set 開頭,且方法僅有一個參數,且方法訪問級別爲 public
                if (method.getName().startsWith("set")
                    && method.getParameterTypes().length == 1
                    && Modifier.isPublic(method.getModifiers())) {
                    // 獲取 setter 方法參數類型
                    Class<?> pt = method.getParameterTypes()[0];
                    try {
                        // 獲取屬性名
                        String property = method.getName().length() > 3 ? 
                            method.getName().substring(3, 4).toLowerCase() + 
                                method.getName().substring(4) : "";
                        // 從 ObjectFactory 中獲取依賴對象
                        Object object = objectFactory.getExtension(pt, property);
                        if (object != null) {
                            // 經過反射調用 setter 方法設置依賴
                            method.invoke(instance, object);
                        }
                    } catch (Exception e) {
                        logger.error("...");
                    }
                }
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}

在上面代碼中,objectFactory 變量的類型爲 AdaptiveExtensionFactory,AdaptiveExtensionFactory 內部維護了一個 ExtensionFactory 列表,用於存儲其餘類型的 ExtensionFactory。Dubbo 目前提供了兩種 ExtensionFactory,分別是 SpiExtensionFactory 和 SpringExtensionFactory。前者用於建立自適應的拓展,關於自適應拓展,我將會在下一篇文章中進行說明。SpringExtensionFactory 則是到 Spring 的 IOC 容器中獲取所需拓展,該類的實現並不複雜,你們自行分析源碼,這裏就很少說了。

Dubbo IOC 的實現比較簡單,僅支持 setter 方式注入。總的來講,邏輯簡單易懂。

4.總結

本篇文章簡單介紹了 Java SPI 與 Dubbo SPI 用法與區別,並對 Dubbo SPI 的部分源碼進行了分析。在 Dubbo SPI 中還有一塊重要的邏輯沒有進行分析,那就是 Dubbo SPI 的擴展點自適應機制。該機制的邏輯較爲複雜,我將會在下一篇文章中進行分析。好了,其餘的就很少說了,本篇文件就先到這裏了。

本文在知識共享許可協議 4.0 下發布,轉載需在明顯位置處註明出處
做者:田小波
本文同步發佈在個人我的博客:http://www.tianxiaobo.com

cc
本做品採用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議進行許可。

相關文章
相關標籤/搜索