SPI(Service Provider Interface)是JDK內置的一種服務提供發現機制。本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣可以在運行時,動態為接口替換實現類。
在Java中SPI是被用來設計給服務提供商做插件使用的。基於策略模式來實現動態加載的機制。我們在程序只定義一個接口,具體的實現交個不同的服務提供者;在程序啟動的時候,讀取配置文件,由配置確定要調用哪一個實現。有很多組件的實現,如日志、數據庫訪問等都是采用這樣的方式,最常用的就是 JDBC 驅動。
1. Java SPI
核心類:java.util.ServiceLoader
服務是一組眾所周知的接口和(通常是抽象的)類。服務提供者是服務的特定實現。提供者中的類通常實現接口,並子類化服務本身中定義的類。服務提供者可以以擴展的形式安裝在Java平台的實現中,即放置在任何常見擴展目錄中的jar文件。提供程序也可以通過將它們添加到應用程序的類路徑或其他特定於平台的方法來提供。
通過在資源目錄META-INF/services中放置一個提供程序配置文件來識別服務提供程序。文件名是服務類型的完全限定二進制名稱。該文件包含具體提供程序類的完全限定二進制名的列表,每行一個。每個名稱周圍的空格和制表符以及空白行將被忽略。注釋字符是'#';在每一行中,第一個注釋字符之后的所有字符都將被忽略。文件必須用UTF-8編碼。
按照上面的方法,我們來寫個例子試一下
首先,定義一個接口Car
package org.example; public interface Car { void run(); }
兩個實現類
ToyotaCar.java
package org.example; public class ToyotaCar implements Car { @Override public void run() { System.out.println("Toyota"); } }
HondaCar.java
package org.example; public class HondaCar implements Car { @Override public void run() { System.out.println("Honda"); } }
在META-INF/services下創建一個名為org.example.Car的文本文件
org.example.ToyotaCar org.example.HondaCar
最后,寫個測試類運行看一下效果
package org.example; import java.util.ServiceLoader; public class App { public static void main( String[] args ) { ServiceLoader<Car> serviceLoader = ServiceLoader.load(Car.class); serviceLoader.forEach(x->x.run()); } }
跟一下ServiceLoader的代碼,看看是怎么找到服務實現的
用當前線程的類加載器加載
接口和類加載器都有了,萬事俱備只欠東風
Java SPI 不足之處:
- 不能按需加載。Java SPI在加載擴展點的時候,會一次性加載所有可用的擴展點,很多是不需要的,會浪費系統資源
- 獲取某個實現類的方式不夠靈活,只能通過 Iterator 形式獲取,不能根據某個參數來獲取對應的實現類
- 不支持AOP與IOC
- 如果擴展點加載失敗,會導致調用方報錯,導致追蹤問題很困難
2. Dubbo SPI
Dubbo重新實現了一套功能更強的SPI機制, 支持了AOP與依賴注入,並且利用緩存提高加載實現類的性能,同時支持實現類的靈活獲取。
<dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo</artifactId> <version>2.7.8</version> </dependency>
核心類:org.apache.dubbo.common.extension.ExtensionLoader
先來了解一下@SPI注解,@SPI是用來標記接口是一個可擴展的接口
改造一下前面的例子,在Car接口上加上@SPI注解
package org.example; import org.apache.dubbo.common.extension.SPI; @SPI public interface Car { void run(); }
兩個實現類不變
在META-INF/dubbo目錄下創建名為org.example.Car的文本文件,內容如下(鍵值對形式):
toyota=org.example.ToyotaCar honda=org.example.HondaCar
編寫測試類
package org.example; import org.apache.dubbo.common.extension.ExtensionLoader; import java.util.ServiceLoader; public class App { public static void main( String[] args ) { // Java SPI ServiceLoader<Car> serviceLoader = ServiceLoader.load(Car.class); serviceLoader.forEach(x->x.run()); // Dubbo SPI ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class); Car car = extensionLoader.getExtension("honda"); car.run(); } }
下面跟一下代碼
如果緩存Map中有,直接返回,沒有則加載完以后放進去
加載策略到底是怎樣的呢?
到這里就有點明白了,又看到了熟悉的ServiceLoad.load(),這不是剛才講的Java SPI嘛
回到之前策略那個地方,將策略按順序排列,依次遍歷所有的策略來加載。就是在那三個目錄下查找指定的文件,並讀取其中的內容
跟之前的ServiceLoader如出一轍
遇到@Adaptive標注的就緩存起來
下課