作者 | 京東云開發(fā)者-京東物流 龔航林
原文鏈接:https://my.oschina.NET/u/4090830/blog/10116011
1 SPI 簡(jiǎn)介1.1 SPI(Service Provider Interface)
本質(zhì):將接口實(shí)現(xiàn)類的全限定名配置在文件中,并由服務(wù)加載器讀取配置文件,加載實(shí)現(xiàn)類。這樣可以在運(yùn)行時(shí),動(dòng)態(tài)為接口替換實(shí)現(xiàn)類。
JAVA SPI:用來設(shè)計(jì)給服務(wù)提供商做插件使用的。基于策略模式來實(shí)現(xiàn)動(dòng)態(tài)加載的機(jī)制。我們?cè)诔绦蛑欢x一個(gè)接口,具體的實(shí)現(xiàn)交個(gè)不同的服務(wù)提供者;在程序啟動(dòng)的時(shí)候,讀取配置文件,由配置確定要調(diào)用哪一個(gè)實(shí)現(xiàn)。
dubbo SPI:在 dubbo 中也有 SPI 機(jī)制,雖然都需要將接口全限定名配置在文件中,但是 dubbo 并沒有使用 java 的 spi 機(jī)制,而是重新實(shí)現(xiàn)了一套功能更強(qiáng)的 SPI 機(jī)制,支持了 AOP 與依賴注入,并且 利用緩存提高加載實(shí)現(xiàn)類的性能,同時(shí) 支持實(shí)現(xiàn)類的靈活獲取。基于 SPI,我們可以很容易的對(duì) Dubbo 進(jìn)行拓展。例如 dubbo 當(dāng)中的 protocol,LoadBalance 等都是通過 SPI 機(jī)制擴(kuò)展。
2 java SPI2.1 實(shí)現(xiàn)過程
1)需要在 classpath 下創(chuàng)建一個(gè)目錄,該目錄命名必須是:META-INF/service
2)在該目錄下創(chuàng)建一個(gè) 文本文件,該文件需要滿足以下幾個(gè)條件
- 文件名必須是擴(kuò)展的接口的全路徑名稱
- 文件內(nèi)部描述的是該擴(kuò)展接口的所有實(shí)現(xiàn)類
- 文件的編碼格式是 UTF-8
3)通過 java.util.ServiceLoader 的加載機(jī)制來加載服務(wù)
2.2 工作原理
1)當(dāng)調(diào)用 ServiceLoader.load (Class clz) 方法時(shí),會(huì)到 jar 中中的目錄 “META-INF/services/“ + clz.getName 進(jìn)行文件讀取,
2)當(dāng)在調(diào)用 ServiceLoader.forEach 方法時(shí),實(shí)際走的是 LazyIterator,當(dāng)在調(diào)用 LazyIterator.hasNext 時(shí),在文件中讀取到實(shí)際的服務(wù)實(shí)現(xiàn)類并把它們通過調(diào)用 Class.forName (String name, boolean initialize,ClassLoader loader)。
2.3 實(shí)際應(yīng)用
javaSPI 我們最熟悉的應(yīng)用就是數(shù)據(jù)庫(kù)驅(qū)動(dòng)了,MySQL 和 oracle 驅(qū)動(dòng)針對(duì) JDBC 分別有自己的實(shí)現(xiàn),這就有賴于 java 的 SPI 機(jī)制。
3 dubbo SPI3.1 實(shí)現(xiàn)過程
1)需要在 classpath 下創(chuàng)建一個(gè)目錄,該目錄命名可以是:META-INF/service/、META-INF/dubbo/、META-INF/dubbo/internal/
2)在該目錄下創(chuàng)建一個(gè) 文本文件,該文件需要滿足以下幾個(gè)條件
- 文件名必須是擴(kuò)展的接口的全路徑名稱
- 文件內(nèi)部描述的是該擴(kuò)展接口的所有實(shí)現(xiàn)類,將服務(wù)實(shí)現(xiàn)類寫成 KV 鍵值對(duì)的形式,Key 是拓展類的 name,Value 是擴(kuò)展的全限定名實(shí)現(xiàn)類。
3)通過 org.Apache.dubbo.common.extension.Extensier 的加載機(jī)制來加載服務(wù)
3.2 工作原理
1)我們首先通過 Extensier 的 getExtensier 方法獲取一個(gè)接口的 Extensier 實(shí)例,然后再通過 Extensier 的 getExtension 方法獲取拓展類對(duì)象,源碼如下,首先是 getExtensier 方法:
new Extensier (type) 源碼如下:
注意這里創(chuàng)建 Extensier 對(duì)象的構(gòu)造方法如下:Extensier.getExtensier 獲取 ExtensionFactory 接口的拓展類,再通過 getAdaptiveExtension 從拓展類中獲取目標(biāo)拓展類。
2)通過 Extensier.getExtensier 取到接口的加載器 Loader 之后,再通過 getExtension 方法獲取需要拓展類對(duì)象。
以上代碼首先檢查 holder 中的實(shí)例緩存,緩存未命中則創(chuàng)建拓展對(duì)象。dubbo 中包含了大量的擴(kuò)展點(diǎn)緩存。這個(gè)就是典型的使用空間換時(shí)間的做法。
創(chuàng)建拓展類對(duì)象步驟分別為:
- 通過 getExtensionClasses 從配置文件中加載所有的拓展類,再通過名稱獲取目標(biāo)拓展類
- 通過反射創(chuàng)建拓展對(duì)象
- 向拓展對(duì)象中注入依賴
- 將拓展對(duì)象包裹在相應(yīng)的 WrApper 對(duì)象中
我們接下來重點(diǎn)看下 getExtensionClasses 方法:
先從緩存中獲取 class,緩存未命中則調(diào)用 loadExtensionClasses 方法加載,我們?cè)倏聪?loadExtensionClasses 這個(gè)方法:
我們看到這里遍歷調(diào)用了多個(gè)策略去加載 class 的,跟到這里我們發(fā)現(xiàn)非常有意思的是:dubbo 在加載 META-INF 目錄下的 class 鍵值對(duì)的時(shí)候采用了 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)、自動(dòng)注入的功能就不在這里過多展開了,有興趣可以自行了解。
3.3 實(shí)際應(yīng)用
dubbo 中大量使用了 SPI 機(jī)制:
例如 dubbo 的多協(xié)議的實(shí)現(xiàn):
4 javaSPI 和 dubboSPI 對(duì)比
- Java SPI 在加載擴(kuò)展點(diǎn)的時(shí)候,會(huì)一次性加載所有可用的擴(kuò)展點(diǎn),很多是不需要的,會(huì)浪費(fèi)系統(tǒng)資源。dubboSPI 有選擇性地加載所需要的 SPI 接口。
- javaSPI 配置文件中只是簡(jiǎn)單的列出了所有的擴(kuò)展實(shí)現(xiàn),而沒有給他們命名。導(dǎo)致在程序中很難去準(zhǔn)確的引用它們。而 dubboSPI 配置文件中以鍵值對(duì)的形式有別名,易于區(qū)分。
- SPI 擴(kuò)展如果依賴其他的擴(kuò)展,javaspi 做不到自動(dòng)注入和裝配,dubbo 可以實(shí)現(xiàn)自動(dòng)注入。
- javaSPI 不提供類似于 Spring 的 IOC 和 AOP 功能,dubboSPI 是支持的