作者:京東物流 孔祥東
1.SPI 是什麼?
SPI 的全稱是Service Provider Interface,即提供服務接口;是一種服務發現機制,SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣可以在運行時,動態爲接口替換實現類。正因此特性,我們可以很容易的通過 SPI 機制爲我們的程序提供拓展功能。
如下圖:
系統設計的各個抽象,往往有很多不同的實現方案,在面對象設計裏,一般推薦模塊之間基於接口編程,模塊之間不對實現硬編碼,一旦代碼涉及具體的實現類,就違反了可插拔的原則。Java SPI 就是提供這樣的一個機制,爲某一個接口尋找服務的實現,有點類似IOC 的思想,把裝配的控制權移到程序之外,在模塊化涉及裏面這個各尤爲重要。與其說SPI 是java 提供的一種服務發現機制,倒不如說是一種解耦思想。
2.使用場景?
- 數據庫驅動加載接口實現類的加載;如:JDBC 加載Mysql,Oracle...
- 日誌門面接口實現類加載,如:SLF4J 對log4j、logback 的支持
- Spring中大量使用了SPI,特別是spring-boot 中自動化配置的實現
- Dubbo 也是大量使用SPI 的方式實現框架的擴展,它是對原生的SPI 做了封裝,允許用戶擴展實現Filter 接口。
3.使用介紹
要使用 Java SPI,需要遵循以下約定:
- 當服務提供者提供了接口的一種具體實現後,需要在JAR 包的META-INF/services 目錄下創建一個以“接口全限制定名”爲命名的文件,內容爲實現類的全限定名;
- 接口實現類所在的JAR放在主程序的classpath 下,也就是引入依賴。
- 主程序通過java.util.ServiceLoder 動態加載實現模塊,它會通過掃描META-INF/services 目錄下的文件找到實現類的全限定名,把類加載值JVM,並實例化它;
- SPI 的實現類必須攜帶一個不帶參數的構造方法。
示例:
spi-interface 模塊定義
定義一組接口:public interface MyDriver
spi-jd-driver
spi-ali-driver
實現爲:public class JdDriver implements MyDriver
public class AliDriver implements MyDriver
在 src/main/resources/ 下建立 /META-INF/services 目錄, 新增一個以接口命名的文件 (org.MyDriver 文件)
內容是要應用的實現類分別 com.jd.JdDriver和com.ali.AliDriver
spi-core
一般都是平臺提供的核心包,包含加載使用實現類的策略等等,我們這邊就簡單實現一下邏輯:a.沒有找到具體實現拋出異常 b.如果發現多個實現,分別打印
public void invoker(){
ServiceLoader<MyDriver> serviceLoader = ServiceLoader.load(MyDriver.class);
Iterator<MyDriver> drivers = serviceLoader.iterator();
boolean isNotFound = true;
while (drivers.hasNext()){
isNotFound = false;
drivers.next().load();
}
if(isNotFound){
throw new RuntimeException("一個驅動實現類都不存在");
}
}
spi-test
public class App
{
public static void main( String[] args )
{
DriverFactory factory = new DriverFactory();
factory.invoker();
}
}
1.引入spi-core 包,執行結果
2.引入spi-core,spi-jd-driver 包
3.引入spi-core,spi-jd-driver,spi-ali-driver
4.原理解析
看看我們剛剛是怎麼拿到具體的實現類的?
就兩行代碼:
ServiceLoader<MyDriver> serviceLoader = ServiceLoader.load(MyDriver.class);
Iterator<MyDriver> drivers = serviceLoader.iterator();
所以,首先我們看ServiceLoader 類:
public final class ServiceLoader<S> implements Iterable<S>{
//配置文件的路徑
private static final String PREFIX = "META-INF/services/";
// 代表被加載的類或者接口
private final Class<S> service;
// 用於定位,加載和實例化providers的類加載器
private final ClassLoader loader;
// 創建ServiceLoader時採用的訪問控制上下文
private final AccessControlContext acc;
// 緩存providers,按實例化的順序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懶查找迭代器,真正加載服務的類
private LazyIterator lookupIterator;
//服務提供者查找的迭代器
private class LazyIterator
implements Iterator<S>
{
.....
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//全限定名:com.xxxx.xxx
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//通過反射獲取
c = Class.forName(cn, false, loader);
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
}
}
........
大概的流程就是下面這張圖:
-
應用程序調用ServiceLoader.load 方法
-
應用程序通過迭代器獲取對象實例,會先判斷providers對象中是否已經有緩存的示例對象,如果存在直接返回
-
如果沒有存在,執行類轉載讀取META-INF/services 下的配置文件,獲取所有能被實例化的類的名稱,可以跨越JAR 獲取配置文件通過反射方法Class.forName()加載對象並用Instance() 方法示例化類將實例化類緩存至providers對象中,同步返回。
5.總結
優點:解耦
SPI 的使用,使得第三方服務模塊的裝配控制邏輯與調用者的業務代碼分離,不會耦合在一起,應用程序可以根據實際業務情況來啓用框架擴展和替換框架組件。
SPI 的使用,使得無須通過下面幾種方式獲取實現類
-
代碼硬編碼import 導入
-
指定類全限定名反射獲取,例如JDBC4.0 之前;Class.forName("com.mysql.jdbc.Driver")
缺點:
雖然ServiceLoader也算是使用的延遲加載,但是基本只能通過遍歷全部獲取,也就是接口的實現類全部加載並實例化一遍。如果你並不想用某些實現類,它也被加載並實例化了,這就造成了浪費。獲取某個實現類的方式不夠靈活,只能通過Iterator形式獲取,不能根據某個參數來獲取對應的實現類。