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