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 的源碼。

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。文件內容為實現類的全限定的類名,如下:

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

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

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。

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 國際許可協議進行許可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM