1 SPI簡介 1.1 SPI(Service Provider Interface) 本質:將介面實現類的全限定名配置在文件中,並由服務載入器讀取配置文件,載入實現類。這樣可以在運行時,動態為介面替換實現類。 java SPI:用來設計給服務提供商做插件使用的。基於策略模式來實現動態載入的機制。我 ...
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 SPI
2.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 SPI
3.1 實現過程
1)需要在 classpath 下創建一個目錄,該目錄命名可以是:META-INF/service/、META-INF/dubbo/、META-INF/dubbo/internal/
2)在該目錄下創建一個 文本文件,該文件需要滿足以下幾個條件
- 文件名必須是擴展的介面的全路徑名稱
- 文件內部描述的是該擴展介面的所有實現類,將服務實現類寫成KV鍵值對的形式,Key是拓展類的name,Value是擴展的全限定名實現類。
3)通過 org.apache.dubbo.common.extension.ExtensionLoader 的載入機制來載入服務
3.2 工作原理
1)我們首先通過 ExtensionLoader的 getExtensionLoader 方法獲取一個介面的 ExtensionLoader 實例,然後再通過 ExtensionLoader 的 getExtension 方法獲取拓展類對象,源碼如下,首先是 getExtensionLoader 方法:
new ExtensionLoader(type)源碼如下:
註意這裡創建 ExtensionLoader對象的構造方法如下:ExtensionLoader.getExtensionLoader獲取ExtensionFactory介面的拓展類,再通過 getAdaptiveExtension從拓展類中獲取目標拓展類。
2)通過 ExtensionLoader.getExtensionLoader取到介面的載入器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是支持的
作者:京東物流 龔航林
來源:京東雲開發者社區 自猿其說Tech 轉載請註明來源