瞭解一下Java SPI的原理
瞭解一下Java SPI的原理
1 為什麼寫這篇文章?
近期,本人在學習dubbo相關的知識,但是在dubbo官網中有提到Java的 SPI,這個名詞之前未接觸過,所以就去看了看,感覺還是有很多地方有使用的,比如jdbc、log相關的技術上均有使用,還是很有用處的,就在這裡總結一下自己的學習內容!(本文有參考相關資料:比如dubbo官網、相關blog等)
2 SPI是什麼?
Java SPI(Service Provider Interface)是JDK內建的一種動態載入擴充套件點的實現。在ClassPath的META-INF/services目錄下放置一個與介面同名的文字檔案,檔案的內容為介面的實現類,多個實現類用換行符分隔。JDK中使用java.util.ServiceLoader來載入具體的實現。
Java SPI 實際上是“基於介面的程式設計+策略模式+配置檔案”組合實現的動態載入機制。
3 自定義一個SPI
3.1 建立工程
建立dubbo-spi的工程,這裡展示一下完整的spi示例程式結構:
3.2 建立介面
在包top.flygrk.ishare.spi.service下建立介面: SPIService
package top.flygrk.ishare.spi.service; /** * @Package top.flygrk.ishare.spi.service * @Version V1.0 * @Description: SPIService 介面 */ public interface SPIService { /** * 介面方法: say() */ String say(); }
3.3 建立實現類: ASPIServiceImpl和BSPIServiceImpl
在包top.flygrk.ishare.spi.service.impl下建立ASPIServiceImpl和BSPIServiceImpl類,均實現SPIservice介面:
- ASPIServiceImpl
package top.flygrk.ishare.spi.service.impl; import top.flygrk.ishare.spi.service.SPIService; /** * @Package top.flygrk.ishare.spi.service.impl * @Version V1.0 * @Description: SPIService 實現類 ASPIServiceImpl */ public class ASPIServiceImpl implements SPIService { @Override public String say() { return "ASPIServiceImpl"; } }
- BSPIServiceImpl
package top.flygrk.ishare.spi.service.impl;
import top.flygrk.ishare.spi.service.SPIService;
/**
* @Package top.flygrk.ishare.spi.service.impl
* @Version V1.0
* @Description: SPIService 實現類 BSPIServiceImpl
*/
public class BSPIServiceImpl implements SPIService {
@Override
public String say() {
return "BSPIServiceImpl";
}
}
3.4 建立檔案top.flygrk.ishare.spi.service.SPIService
在resource目錄下,建立META-INF/services目錄,並在該目錄下建立top.flygrk.ishare.spi.service.SPIService檔案(該檔名為介面的全路徑,需保持一致),並在該檔案中配置兩個實現類的全路徑:
top.flygrk.ishare.spi.service.impl.ASPIServiceImpl
top.flygrk.ishare.spi.service.impl.BSPIServiceImpl
3.5 建立測試類TestSPIService
在包top.flygrk.ishare.demo下建立TestSPIService類,用於測試該SPI服務
package top.flygrk.ishare.demo;
import top.flygrk.ishare.spi.service.SPIService;
import java.util.Iterator;
import java.util.ServiceLoader;
/**
* @Package top.flygrk.ishare.demo
* @Version V1.0
* @Description: 測試 SPIService
*/
public class TestSPIService {
public static void main(String[] args) {
// ServiceLoader實現了Iterable介面,可以遍歷出所有的服務實現者
ServiceLoader<SPIService> serviceLoaders = ServiceLoader.load(SPIService.class);
/*
* 方法1: 迭代器
*/
Iterator<SPIService> spiServiceIterator = serviceLoaders.iterator();
while (spiServiceIterator != null && spiServiceIterator.hasNext()) {
SPIService spiService = spiServiceIterator.next();
System.out.println(spiService.getClass().getName() + " : " + spiService.say());
}
/*
* 迭代方法2: foreach
*/
// for (SPIService spiService : serviceLoaders) {
// System.out.println(spiService.getClass().getName() + " : " + spiService.say());
// }
}
}
3.6 測試類執行結果
top.flygrk.ishare.spi.service.impl.ASPIServiceImpl : ASPIServiceImpl
top.flygrk.ishare.spi.service.impl.BSPIServiceImpl : BSPIServiceImpl
4 SPI原理分析
在我們閱讀原始碼前,我們先提出以下幾個問題,然後我們再去帶著問題去原始碼中找答案:
-
- META-INF/services目錄下的檔案有什麼用?為什麼要用介面的全路徑命名?是否可以更改介面名稱?裡面的內容為什麼要用實現類的全路徑?
- 2) ServiceLoader 是如何獲取到SPIService的全部實現的?
- 3) 如果我們只想取ASPIServiceImpl,並不想去操作BSPIServiceImpl,如何去操作?
4.1 ServiceLoader結構
我們先看一下ServiceLoader類的結構:
進入ServiceLoader類的原始碼,我們可以看到以下定義的一些常量:
各位肯定注意到了一點: private static final String PREFIX = "META-INF/services/";
, 這個PREFIX後面的路徑不正是我們在上述示例中建立和介面保持一致的檔案的目錄嗎?還有services、loader、acc、lookupIterator和providers表達的意思在原始碼上方的註釋中也進行了描述,下面我將各個屬性的釋義標註一下:
// 配置檔案的目錄
private static final String PREFIX = "META-INF/services/";
// 要載入服務的類或者介面
// The class or interface representing the service being loaded
private final Class<S> service;
// 服務載入器
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// 訪問控制上下文
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// 服務例項的快取
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懶載入的迭代器
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
4.2 ServiceLoader的載入過程
看完了上面ServiceLoader的結構,下面我們再來看看ServiceLoader是如何一步步載入的。我們在TestSPIService類上的main方法第一行打上斷點:
然後使用debug的方式除錯,進入ServiceLoader的原始碼,會依次進入以下幾個函式:
經過這些步驟之後,serviceLoader內部包含有一個Iterator迭代器,下面我們來仔細看一下這個迭代器的作用!
4.3 迭代器lookupIterator的操作
在上述4.2步驟載入完成之後,serviceLoader內的lookupIterator的內容如下:
然後使用iterator()方法獲取Iterator迭代器時,執行如下的程式:
在經過上述過程之後,我們拿到了Iterator迭代器,這時我們看下spiServiceIterator的內容:
是不是很奇怪,還是隻有SPIService,不要忘記了,他內部的迭代器可是懶載入的!我們繼續跟進程式碼,進入到hasNext()方法。
從上面可以知道,acc一直為null的,所以這時候,他進入了hasNextService()方法:
重頭戲來了,我們可以看到其中的 PREFIX, 這個內容就是我們配置的檔案。再仔細的跟進程式碼,我們會進入到parse()方法,該方法用於按照行讀取出檔案中的內容,並儲存到Iterator<String>
中。
故而,再通過 nextName = pending.next();
執行後,獲取到top.flygrk.ishare.spi.service.impl.ASPIServiceImpl
,繼而進行後續的next()方法操作。
然後進入到nextService()方法:
再nextService()方法裡,使用了反射的技術,根據前面從檔案中讀取到的實現類全路徑top.flygrk.ishare.spi.service.impl.ASPIServiceImpl
獲取到該實現類的物件!走到這裡,也就基本上了解了SPI,但是我們能只獲取ASPIServiceImpl,而不去獲取BSPIServiceImpl嗎?對不起,這裡不允許這樣,只能通過迭代器遍歷出所有的內容!除非人為干預(外層迴圈比對完成之後退出迴圈)。接下來的步驟就和前面幾乎一致了,這裡不再細述~
5 SPI 優缺點
我們評價一門思想往往需要從其優缺點的方向進行考慮。SPI同樣也是有一定的優缺點存在的,下面我們來仔細的看下它有哪些優缺點:
5.1 優點
- 解耦:最大的優點也就是解耦了,通過SPI可以使第三方服務模組的邏輯與業務程式碼相分離,而不耦合在一起。應用程式可以根據實際業務進行擴充套件。
5.2 缺點
參考dubbo官方文件
- 需要遍歷所有的實現,並例項化,然後我們在迴圈中才能找到我們需要的實現。
- 配置檔案中只是簡單的列出了所有的擴充套件實現,而沒有給他們命名。導致在程式中很難去準確的引用它們。
- 擴充套件如果依賴其他的擴充套件,做不到自動注入和裝配
- 不提供類似於Spring的IOC和AOP功能
- 擴充套件很難和其他的框架整合,比如擴充套件裡面依賴了一個Spring bean,原生的Java SPI不支援
6 SPI案例分析
在我們常用的框架中,有很多都是有使用SPI的方式,其中包括JDBC載入不同型別資料庫的驅動、SLF4J載入不同提供商的日誌實現類、Spring 框架、Dubbo框架。
這裡需要注意,dubbo框架的SPI是對原生的Java SPI 進行了擴充套件的。關於dubbo的SPI我們將在後面詳細講解。現在,我們來以JDBC載入的方式來簡單的看看其SPI的方式。
我們先找到mysql的包,其結構如下:
在META-INF/services 目錄下,存在 檔案 java.sql.Driver,其內容為:
通過這個路徑,我們也可以找到 com.mysql.jdbc.Driver類,它實現了java.sql.Driver介面:
諸如Oracle,同樣也有此機制,這裡就不再細述了,請自行驗證檢視~