SPI是什麼
SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴展的API,它可以用來啓用框架擴展和替換組件。
Java SPI 實際上是“基於接口的編程+策略模式+配置文件”組合實現的動態加載機制。
系統設計的各個抽象,往往有很多不同的實現方案,在面向的對象的設計裏,一般推薦模塊之間基於接口編程,模塊之間不對實現類進行硬編碼。一旦代碼裏涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改代碼。爲了實現在模塊裝配的時候能不在程序裏動態指明,這就需要一種服務發現機制。
Java SPI就是提供這樣的一個機制:爲某個接口尋找服務實現的機制。有點類似IOC的思想,就是將裝配的控制權移到程序之外,在模塊化設計中這個機制尤其重要。所以SPI的核心思想就是解耦。
使用場景
概括地說,適用於:調用者根據實際使用需要,啓用、擴展、或者替換框架的實現策略
比較常見的例子:
- 數據庫驅動加載接口實現類的加載
JDBC加載不同類型數據庫的驅動 - 日誌門面接口實現類加載
SLF4J加載不同提供商的日誌實現類 - Spring
Spring中大量使用了SPI,比如:對servlet3.0規範對ServletContainerInitializer的實現、自動類型轉換Type Conversion SPI(Converter SPI、Formatter SPI)等 - Dubbo
Dubbo中也大量使用SPI的方式實現框架的擴展, 不過它對Java提供的原生SPI做了封裝,允許用戶擴展實現Filter接口
實際案例
- 定義一個接口IPerson
package com.alioo.spi;
public interface IPerson {
void sayHello();
}
- 編寫一個程序入口類SPIDemo
在這個程序入口類裏獲取該接口的實現類,並進行調用
package com.alioo.spi;
import java.util.Iterator;
import java.util.ServiceLoader;
public class SPIDemo {
public static void main(String[] args) {
ServiceLoader<IPerson> shouts = ServiceLoader.load(IPerson.class);
System.out.println("shouts:" + shouts);
Iterator<IPerson> it = shouts.iterator();
while (it.hasNext()) {
IPerson s = it.next();
System.out.println("s:" + s);
s.sayHello();
}
}
}
備註:
事實上到目前爲止我們還沒有這個接口的任何實現類,然而上面的代碼不影響我們編寫、編譯打包的
- 編寫接口的實現類Man
package com.alioo.spi;
public class Man implements IPerson {
@Override
public void sayHello() {
System.out.println("我是男人");
}
}
還需要創建一個META-INF/services目錄,並在該目錄下創建一個文件,文件名爲接口IPerson的包路徑,即com.alioo.spi.IPerson,文件內容爲實現類的包路徑
│ ├── pom.xml
│ └── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── alioo
│ │ └── spi
│ │ └── Man.java
│ └── resources
│ └── META-INF
│ └── services
│ └── com.alioo.spi.IPerson
# more src/main/resources/META-INF/services/com.alioo.spi.IPerson
com.alioo.spi.Man
備註:
這個實現類Man可以獨立於上面的程序入口類SPIDemo而存在,比如將Man.class和META-INF目錄單獨打到一個jar裏,只需要SPIDemo指定classpath包含這個jar可以
- 運行效果
shouts:java.util.ServiceLoader[com.alioo.spi.IPerson]
s:com.alioo.spi.Man@610455d6
我是男人
- 引入更多的實現類
比如再增加一個實現類Woman
package com.alioo.spi;
public class Woman implements IPerson {
@Override
public void sayHello() {
System.out.println("我是女人");
}
}
同步驟4,增加META-INF/services目錄及文件com.alioo.spi.IPerson,文件內容爲當前實現類的包路徑
│ ├── pom.xml
│ └── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── alioo
│ │ └── spi
│ │ └── Woman.java
│ └── resources
│ └── META-INF
│ └── services
│ └── com.alioo.spi.IPerson
more src/main/resources/META-INF/services/com.alioo.spi.IPerson
com.alioo.spi.Woman
這個時候的運行效果如下:
shouts:java.util.ServiceLoader[com.alioo.spi.IPerson]
s:com.alioo.spi.Man@610455d6
我是男人
s:com.alioo.spi.Woman@60e53b93
我是女人
總結
優點
使用Java SPI機制的優勢是實現解耦,使得第三方服務模塊的裝配控制的邏輯與調用者的業務代碼分離,而不是耦合在一起。應用程序可以根據實際業務情況啓用框架擴展或替換框架組件。
相比使用提供接口jar包,供第三方服務模塊實現接口的方式,SPI的方式使得源框架,不必關心接口的實現類的路徑,可以不用通過下面的方式獲取接口實現類:
- 代碼硬編碼import導入實現類
- 指定類全路徑反射獲取:例如在JDBC4.0之前,JDBC中獲取數據庫驅動類需要通過Class.forName(“com.mysql.jdbc.Driver”),類似語句先動態加載數據庫相關的驅動,然後再進行獲取連接等的操作
- 第三方服務模塊把接口實現類實例註冊到指定地方,源框架從該處訪問實例
- 通過SPI的方式,第三方服務模塊實現接口後,在第三方的項目代碼的META-INF/services目錄下的配置文件指定實現類的全路徑名,源碼框架即可找到實現類
缺點
- 雖然ServiceLoader也算是使用的延遲加載 ,但是基本只能通過遍歷全部獲取,也就是接口的實現類全部加載並實例化一遍。如果你並不想用某些實現類,它也被加載並實例化了,這就造成了浪費。獲取某個實現類的方式不夠靈活,只能通過Iterator形式獲取,不能根據某個參數來獲取對應的實現類。
- 多個併發多線程使用ServiceLoader類的實例是不安全的。
使用場景附加說明
java.sql.DriverManager 含有static代碼塊,當該類被加載時,會觸發調用loadInitialDrivers()方法
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
在loadInitialDrivers()方法中含有jdk spi的調用
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
通過這種方式再也不用向以前那樣調用這句話了```Class.forName(“com.mysql.jdbc.Driver”)``
幾個問題
既然這麼方便,爲啥之前就得加Class.forName(…)了呢
因爲JDK SPI技術是從jdk6纔開始有的(閱讀下ServiceLoader類的註釋就可以看到了)
參考文章
作者:分佈式系統架構
鏈接:https://www.jianshu.com/p/46b42f7f593c
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。