Dubbo 原始碼分析 - SPI 機制

原始碼老司機發表於2019-05-07

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 方法的邏輯稍複雜一下,包含了如下的步驟:

通過 getExtensionClasses 獲取所有的擴充類 通過反射建立擴充物件 向擴充物件中注入依賴 將擴充物件包裹在相應的 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 的擴充套件點自適應機制。該機制的邏輯較為複雜,我將會在下一篇文章中進行分析。好了,其他的就不多說了,本篇檔案就先到這裡了。 Java技術交流群 687721065(備註:CSDN): Java網際網路技術

相關文章