作者 | 京東云開發(fā)者-京東物流 龔航林
原文鏈接:https://my.oschina.NET/u/4090830/blog/10116011
1 SPI 簡介1.1 SPI(Service Provider Interface)
本質(zhì):將接口實現(xiàn)類的全限定名配置在文件中,并由服務(wù)加載器讀取配置文件,加載實現(xiàn)類。這樣可以在運行時,動態(tài)為接口替換實現(xiàn)類。
JAVA SPI:用來設(shè)計給服務(wù)提供商做插件使用的。基于策略模式來實現(xiàn)動態(tài)加載的機(jī)制。我們在程序只定義一個接口,具體的實現(xiàn)交個不同的服務(wù)提供者;在程序啟動的時候,讀取配置文件,由配置確定要調(diào)用哪一個實現(xiàn)。
dubbo SPI:在 dubbo 中也有 SPI 機(jī)制,雖然都需要將接口全限定名配置在文件中,但是 dubbo 并沒有使用 java 的 spi 機(jī)制,而是重新實現(xiàn)了一套功能更強(qiáng)的 SPI 機(jī)制,支持了 AOP 與依賴注入,并且 利用緩存提高加載實現(xiàn)類的性能,同時 支持實現(xiàn)類的靈活獲取。基于 SPI,我們可以很容易的對 Dubbo 進(jìn)行拓展。例如 dubbo 當(dāng)中的 protocol,LoadBalance 等都是通過 SPI 機(jī)制擴(kuò)展。
2 java SPI2.1 實現(xiàn)過程
1)需要在 classpath 下創(chuàng)建一個目錄,該目錄命名必須是:META-INF/service
2)在該目錄下創(chuàng)建一個 文本文件,該文件需要滿足以下幾個條件
- 文件名必須是擴(kuò)展的接口的全路徑名稱
- 文件內(nèi)部描述的是該擴(kuò)展接口的所有實現(xiàn)類
- 文件的編碼格式是 UTF-8
3)通過 java.util.ServiceLoader 的加載機(jī)制來加載服務(wù)


2.2 工作原理
1)當(dāng)調(diào)用 ServiceLoader.load (Class clz) 方法時,會到 jar 中中的目錄 “META-INF/services/“ + clz.getName 進(jìn)行文件讀取,
2)當(dāng)在調(diào)用 ServiceLoader.forEach 方法時,實際走的是 LazyIterator,當(dāng)在調(diào)用 LazyIterator.hasNext 時,在文件中讀取到實際的服務(wù)實現(xiàn)類并把它們通過調(diào)用 Class.forName (String name, boolean initialize,ClassLoader loader)。

2.3 實際應(yīng)用
javaSPI 我們最熟悉的應(yīng)用就是數(shù)據(jù)庫驅(qū)動了,MySQL 和 oracle 驅(qū)動針對 JDBC 分別有自己的實現(xiàn),這就有賴于 java 的 SPI 機(jī)制。

3 dubbo SPI3.1 實現(xiàn)過程
1)需要在 classpath 下創(chuàng)建一個目錄,該目錄命名可以是:META-INF/service/、META-INF/dubbo/、META-INF/dubbo/internal/
2)在該目錄下創(chuàng)建一個 文本文件,該文件需要滿足以下幾個條件
- 文件名必須是擴(kuò)展的接口的全路徑名稱
- 文件內(nèi)部描述的是該擴(kuò)展接口的所有實現(xiàn)類,將服務(wù)實現(xiàn)類寫成 KV 鍵值對的形式,Key 是拓展類的 name,Value 是擴(kuò)展的全限定名實現(xiàn)類。
3)通過 org.Apache.dubbo.common.extension.Extensier 的加載機(jī)制來加載服務(wù)


3.2 工作原理
1)我們首先通過 Extensier 的 getExtensier 方法獲取一個接口的 Extensier 實例,然后再通過 Extensier 的 getExtension 方法獲取拓展類對象,源碼如下,首先是 getExtensier 方法:

new Extensier (type) 源碼如下:
注意這里創(chuàng)建 Extensier 對象的構(gòu)造方法如下:Extensier.getExtensier 獲取 ExtensionFactory 接口的拓展類,再通過 getAdaptiveExtension 從拓展類中獲取目標(biāo)拓展類。
2)通過 Extensier.getExtensier 取到接口的加載器 Loader 之后,再通過 getExtension 方法獲取需要拓展類對象。

以上代碼首先檢查 holder 中的實例緩存,緩存未命中則創(chuàng)建拓展對象。dubbo 中包含了大量的擴(kuò)展點緩存。這個就是典型的使用空間換時間的做法。

創(chuàng)建拓展類對象步驟分別為:
- 通過 getExtensionClasses 從配置文件中加載所有的拓展類,再通過名稱獲取目標(biāo)拓展類
- 通過反射創(chuàng)建拓展對象
- 向拓展對象中注入依賴
- 將拓展對象包裹在相應(yīng)的 WrApper 對象中
我們接下來重點看下 getExtensionClasses 方法:

先從緩存中獲取 class,緩存未命中則調(diào)用 loadExtensionClasses 方法加載,我們再看下 loadExtensionClasses 這個方法:
我們看到這里遍歷調(diào)用了多個策略去加載 class 的,跟到這里我們發(fā)現(xiàn)非常有意思的是:dubbo 在加載 META-INF 目錄下的 class 鍵值對的時候采用了 javaSPI 的方式


這里 dubbo 使用 javaSPI 的方式加載到 3 中類加載策略:
org.apache.dubbo.common.extension.DubboInternalLoadingStrategy 用于加載 META-INF/dubbo/internal/ 中的 class
org.apache.dubbo.common.extension.DubboLoadingStrategy 用于加載 META-INF/dubbo/ 中的 class
org.apache.dubbo.common.extension.ServicesLoadingStrategy 用于加載 META-INF/service/ 中的 class
dubbo 的 SPI 還提供了自適應(yīng)(Adaptive)、自動注入的功能就不在這里過多展開了,有興趣可以自行了解。
3.3 實際應(yīng)用
dubbo 中大量使用了 SPI 機(jī)制:

例如 dubbo 的多協(xié)議的實現(xiàn):

4 javaSPI 和 dubboSPI 對比
- Java SPI 在加載擴(kuò)展點的時候,會一次性加載所有可用的擴(kuò)展點,很多是不需要的,會浪費系統(tǒng)資源。dubboSPI 有選擇性地加載所需要的 SPI 接口。
- javaSPI 配置文件中只是簡單的列出了所有的擴(kuò)展實現(xiàn),而沒有給他們命名。導(dǎo)致在程序中很難去準(zhǔn)確的引用它們。而 dubboSPI 配置文件中以鍵值對的形式有別名,易于區(qū)分。
- SPI 擴(kuò)展如果依賴其他的擴(kuò)展,javaspi 做不到自動注入和裝配,dubbo 可以實現(xiàn)自動注入。
- javaSPI 不提供類似于 Spring 的 IOC 和 AOP 功能,dubboSPI 是支持的






