Dubbo原始碼解析之SPI(一):擴充套件類的載入過程

宜信技術學院發表於2020-03-05

Dubbo是一款開源的、高效能且輕量級的Java RPC框架,它提供了三大核心能力:面向介面的遠端方法呼叫、智慧容錯和負載均衡,以及服務自動註冊和發現。

Dubbo最早是阿里公司內部的RPC框架,於 2011 年開源,之後迅速成為國內該類開源專案的佼佼者,2018年2月,透過投票正式成為 Apache基金會孵化專案。目前宜信公司內部也有不少專案在使用Dubbo。

本系列文章透過拆解Dubbo原始碼,幫助大家瞭解Dubbo,做到知其然,並且知其所以然。

一、JDK SPI

1.1 什麼是SPI?

SPI(Service Provider Interface),即服務提供方介面,是JDK內建的一種服務提供機制。在寫程式的時候,一般都推薦面向介面程式設計,這樣做的好處是:降低了程式的耦合性,有利於程式的擴充套件。

SPI也秉承這種理念,提供了統一的服務介面,服務提供商可以各自提供自己的具體實現。大家都熟知的JDBC中用的就是基於這種機制來發現驅動提供商,不管是Oracle也好,MySQL也罷,在編寫程式碼時都一樣,只不過引用的jar包不同而已。後來這種理念也被運用於各種架構之中,比如Dubbo、Eleasticsearch。

1.2 JDK SPI的小栗子

SPI 的實現方式是將介面實現類的全限定名配置在檔案中,由服務載入器讀取配置檔案,載入實現類。  

瞭解了概念後,來看一個具體的例子。

1)定義一個介面

public interface Operation {
        int operate(int num1, int num2);
}

2)寫兩個簡單的實現

public class DivisionOperation implements Operation {
        public int operate(int num1, int num2) {
            System.out.println("run division operation");
            return num1/num2;
        }
}

3)新增一個配置檔案

在ClassPath路徑下新增一個配置檔案,檔名字是介面的全限定類名,內容是實現類的全限定類名,多個實現類用換行符分隔。

目錄結構

檔案內容

com.api.impl.DivisionOperation
com.api.impl.PlusOperation

4)測試程式

public class JavaSpiTest {
    @Test
    public void testOperation() throws Exception {
        ServiceLoader<Operation> operations = ServiceLoader.load(Operation.class);
        operations.forEach(item->System.out.println("result: " + item.operate(2, 2)));
    } 
}

5)測試結果

run division operation
result:1
run plus operation
result:4

1.3 JDK SPI的原始碼分析

例子很簡單,實現的話,可以大膽猜測一下,看名字“ServiceLoader”應該就是用類載入器根據介面的型別加上配置檔案裡的具體實現名字將實現載入了進來。

接下來透過分析原始碼進一步瞭解其實現原理。

1.3.1 ServiceLoader類

PREFIX定義了載入路徑,reload方法初始化了LazyIterator,LazyIterator是載入的核心,真正實現了載入。載入的模式從名字上就可以看出,是懶載入的模式,只有當真正呼叫迭代時才會載入。

1.3.2 hasNextService方法

LazyIterator中的hasNextService方法負責載入配置檔案和解析具體的實現類名。  

1.3.3 nextService方法

LazyIterator中的nextService方法負責用反射載入實現類。

看完了原始碼,感覺這個程式碼是有最佳化空間的,例項化所有實現其實沒啥必要,一來比較耗時,二來浪費資源。Dubbo就沒有使用Java原生的SPI機制,而是對其進行了增強,使其能夠更好地滿足需求。  

二、Dubbo SPI

2.1 Dubbo SPI的小栗子

老習慣,在拆解原始碼之前,先來個栗子。此處示例是在前文例子的基礎上稍做了些修改。

1)定義一個介面

修改介面,加上了Dubbo的@SPI註解。

@SPI
public interface Operation {
        int operate(int num1, int num2);
}

2)寫兩個簡單的實現

沿用之前的兩個實現類。

3)新增一個配置檔案

新增配置檔案放在dubbo目錄下。

目錄結構

檔案內容

division=com.api.impl.DivisionOperation
plus=com.api.impl.PlusOperation

4)測試程式

public class DubboSpiTest {
    @Test
    public void testOperation() throws Exception {
          ExtensionLoader<Operation> loader = ExtensionLoader.getExtensionLoader(Operation.class);
          Operation division = loader.getExtension("division");
          System.out.println("result: " + division.operate(1, 2));
    } 
}

5)測試結果

run division operation
result:0

2.2 Dubbo SPI原始碼

上面的測試例子也很簡單,和JDK原生的SPI對比來看,Dubbo的SPI可以根據配置的kv值來獲取。在沒有拆解原始碼之前,考慮一下如何實現。

我可能會用雙層Map來實現快取:第一層的key為介面的class物件,value為一個map;第二層的key為副檔名(配置檔案中的key),value為實現類的class。實現懶載入的方式,當執行方法的時候建立空map。在真正獲取時先從快取中查詢具體實現類的class物件,找得到就直接返回、找不到就根據配置檔案載入並快取。

Dubbo又是如何實現的呢?

2.2.1 getExtensionLoader方法

首先來拆解getExtensionLoader方法。

這是一個靜態的工廠方法,要求傳入的型別必須為介面並且有SPI的註解,用map做了個快取,key為介面的class物件,而value是 ExtensionLoader物件。

2.2.2 getExtension方法

再來拆解ExtensionLoader的getExtension方法。

這段程式碼也不復雜,如果傳入的引數為'true',則返回預設的擴充套件類例項;否則,從快取中獲取例項,如果有就從快取中獲取,沒有的話就新建。用map做快取,快取了holder物件,而holder物件中存放擴充套件類。用volatile關鍵字和雙重檢查來應對多執行緒建立問題,這也是單例模式的常用寫法。

2.2.3 createExtension方法

重點分析createExtension方法。

這段程式碼由幾部分組成:

  • 根據傳入的副檔名獲取對應的class。
  • 根據class去快取中獲取例項,如果沒有的話,透過反射建立物件並放入快取。
  • 依賴注入,完成物件例項的初始化。
  • 建立wrapper物件。也就是說,此處返回的物件不一定是具體的實現類,可能是包裝的物件。

第二個沒啥好說的,我們重點來分析一下1、3、4三個部分。

1)getExtensionClasses方法

老套路,從快取獲取,沒有的話建立並加入快取。這裡快取的是一個副檔名和class的關係。這個副檔名就是在配置檔案中的key。建立之前,先快取了一下介面的限定名。載入配置檔案的路徑是以下這幾個。

2)loadDirectory方法

獲取配置檔案路徑,獲取classLoader,並使用loadResource方法做進一步處理。

3)loadResource方法

loadResource載入了配置檔案,並解析了配置檔案中的內容。loadClass 方法操作了不同的快取。

首先判斷是否有Adaptive註解,有的話快取到cacheAdaptiveClass(快取結構為class);然後判斷是否wrapperclasses,是的話快取到cacheWrapperClass中(快取結構為Set);如果以上都不是,這個類就是個普通的類,儲存class和名稱的對映關係到cacheNames裡(快取結構為Map)。

基本上getExtensionClasses方法就分析完了,可以看出來,其實並不是很複雜。

2.2.4 IOC

1)injectExtension方法

這個方法實現了依賴注入,即IOC。首先透過反射獲取到例項的方法;然後遍歷,獲取setter方法;接著從objectFactory中獲取依賴物件;最後透過反射呼叫setter方法注入依賴。

objectFactory的變數型別為AdaptiveExtensionFactory。

2)AdaptiveExtensionFactory

這個類裡面有個ExtensionFactory的列表,用來儲存其他型別的 ExtensionFactory。Dubbo提供了兩種ExtensionFactory,一種是SpiExtensionFactory, 用於建立自適應的擴充套件;另一種是SpringExtesionFactory,用於從Spring的IOC容器中獲取擴充套件。配置檔案一個在dubbo-common模組,一個在dubbo-config模組。  

配置檔案

SpiExtensionFactory中的Spi方式前面已經解析過了。  

SpringExtesionFactory是從ApplicationContext中獲取對應的例項。先根據名稱查詢,找不到的話,再根據型別查詢。

依賴注入的部分也拆解完畢,看看這次拆解的最後一部分程式碼。

2.2.5 AOP

建立wrapper物件的部分,wrapper物件是從哪裡來的呢?還記得之前拆解的第一步麼,loadClass方法中有幾個快取,其中wrapperclasses就是快取這些wrapper的class。

從程式碼中可以看出,只要構造方法裡有且只有唯一引數,同時此引數為當前傳入的介面型別,即為wrapper class。

此處迴圈建立wrapper例項,首先將instance做為建構函式的引數,透過反射來建立wrapper物件,然後再向wrapper中注入依賴。

看到這裡,可能會有人有疑問:為什麼要建立一個wrapper物件?其實很簡單,系統要在真正呼叫的前後乾點別的事唄。這個就有點類似於spring的aop了。

三、總結

本文簡單介紹了JDK的SPI和Dubbo的SPI用法,分析了JDK的SPI原始碼和Dubbo的SPI原始碼。在拆解的過程中可以看出,Dubbo的原始碼還是很值得一讀的。在實現方面考慮得很周全,不僅有對多執行緒的處理、多層快取,也有IOC、AOP的過程。不過,Dubbo的SPI就這麼簡單麼?當然不是,這篇只拆解了擴充套件類的載入過程,Dubbo的SPI中還有個很複雜的擴充套件點-自適應機制。欲知後事如何,請聽下回分解~~

本文作者:宜信支付結算部支付研發團隊Java研發高階工程師鄭祥斌

原文首發於「野指標」


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69918724/viewspace-2678683/,如需轉載,請註明出處,否則將追究法律責任。

相關文章