了解一下Java SPI的原理
1 為什么寫這篇文章含懊?
近期,本人在學(xué)習(xí)dubbo相關(guān)的知識,但是在dubbo官網(wǎng)中有提到Java的 SPI,這個名詞之前未接觸過蚊伞,所以就去看了看,感覺還是有很多地方有使用的吮铭,比如jdbc、log相關(guān)的技術(shù)上均有使用颅停,還是很有用處的谓晌,就在這里總結(jié)一下自己的學(xué)習(xí)內(nèi)容!(本文有參考相關(guān)資料:比如dubbo官網(wǎng)癞揉、相關(guān)blog等)
2 SPI是什么纸肉?
Java SPI(Service Provider Interface)是JDK內(nèi)置的一種動態(tài)加載擴(kuò)展點的實現(xiàn)。在ClassPath的META-INF/services目錄下放置一個與接口同名的文本文件喊熟,文件的內(nèi)容為接口的實現(xiàn)類柏肪,多個實現(xiàn)類用換行符分隔。JDK中使用java.util.ServiceLoader來加載具體的實現(xiàn)芥牌。
Java SPI 實際上是“基于接口的編程+策略模式+配置文件”組合實現(xiàn)的動態(tài)加載機(jī)制烦味。
3 自定義一個SPI
3.1 創(chuàng)建工程
創(chuàng)建dubbo-spi的工程,這里展示一下完整的spi示例程序結(jié)構(gòu):
3.2 創(chuàng)建接口
在包top.flygrk.ishare.spi.service下創(chuàng)建接口: 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 創(chuàng)建實現(xiàn)類: ASPIServiceImpl和BSPIServiceImpl
在包top.flygrk.ishare.spi.service.impl下創(chuàng)建ASPIServiceImpl和BSPIServiceImpl類壁拉,均實現(xiàn)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 實現(xiàn)類 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 實現(xiàn)類 BSPIServiceImpl
*/
public class BSPIServiceImpl implements SPIService {
@Override
public String say() {
return "BSPIServiceImpl";
}
}
3.4 創(chuàng)建文件top.flygrk.ishare.spi.service.SPIService
在resource目錄下谬俄,創(chuàng)建META-INF/services目錄,并在該目錄下創(chuàng)建top.flygrk.ishare.spi.service.SPIService文件(該文件名為接口的全路徑弃理,需保持一致)溃论,并在該文件中配置兩個實現(xiàn)類的全路徑:
top.flygrk.ishare.spi.service.impl.ASPIServiceImpl
top.flygrk.ishare.spi.service.impl.BSPIServiceImpl
3.5 創(chuàng)建測試類TestSPIService
在包top.flygrk.ishare.demo下創(chuàng)建TestSPIService類,用于測試該SPI服務(wù)
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實現(xiàn)了Iterable接口痘昌,可以遍歷出所有的服務(wù)實現(xiàn)者
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 測試類運行結(jié)果
top.flygrk.ishare.spi.service.impl.ASPIServiceImpl : ASPIServiceImpl
top.flygrk.ishare.spi.service.impl.BSPIServiceImpl : BSPIServiceImpl
4 SPI原理分析
在我們閱讀源碼前钥勋,我們先提出以下幾個問題炬转,然后我們再去帶著問題去源碼中找答案:
- META-INF/services目錄下的文件有什么用?為什么要用接口的全路徑命名算灸?是否可以更改接口名稱扼劈?里面的內(nèi)容為什么要用實現(xiàn)類的全路徑?
- 2) ServiceLoader 是如何獲取到SPIService的全部實現(xiàn)的乎婿?
- 3) 如果我們只想取ASPIServiceImpl测僵,并不想去操作BSPIServiceImpl,如何去操作谢翎?
4.1 ServiceLoader結(jié)構(gòu)
我們先看一下ServiceLoader類的結(jié)構(gòu):
進(jìn)入ServiceLoader類的源碼捍靠,我們可以看到以下定義的一些常量:
各位肯定注意到了一點: private static final String PREFIX = "META-INF/services/";
, 這個PREFIX后面的路徑不正是我們在上述示例中創(chuàng)建和接口保持一致的文件的目錄嗎森逮?還有services榨婆、loader、acc褒侧、lookupIterator和providers表達(dá)的意思在源碼上方的注釋中也進(jìn)行了描述良风,下面我將各個屬性的釋義標(biāo)注一下:
// 配置文件的目錄
private static final String PREFIX = "META-INF/services/";
// 要加載服務(wù)的類或者接口
// The class or interface representing the service being loaded
private final Class<S> service;
// 服務(wù)加載器
// 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;
// 服務(wù)實例的緩存
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懶加載的迭代器
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
4.2 ServiceLoader的加載過程
看完了上面ServiceLoader的結(jié)構(gòu),下面我們再來看看ServiceLoader是如何一步步加載的闷供。我們在TestSPIService類上的main方法第一行打上斷點:
然后使用debug的方式調(diào)試烟央,進(jìn)入ServiceLoader的源碼,會依次進(jìn)入以下幾個函數(shù):
經(jīng)過這些步驟之后歪脏,serviceLoader內(nèi)部包含有一個Iterator迭代器疑俭,下面我們來仔細(xì)看一下這個迭代器的作用!
4.3 迭代器lookupIterator的操作
在上述4.2步驟加載完成之后婿失,serviceLoader內(nèi)的lookupIterator的內(nèi)容如下:
然后使用iterator()方法獲取Iterator迭代器時钞艇,執(zhí)行如下的程序:
在經(jīng)過上述過程之后,我們拿到了Iterator迭代器豪硅,這時我們看下spiServiceIterator的內(nèi)容:
是不是很奇怪哩照,還是只有SPIService,不要忘記了懒浮,他內(nèi)部的迭代器可是懶加載的飘弧!我們繼續(xù)跟進(jìn)代碼,進(jìn)入到hasNext()方法嵌溢。
從上面可以知道眯牧,acc一直為null的,所以這時候赖草,他進(jìn)入了hasNextService()方法:
重頭戲來了学少,我們可以看到其中的 PREFIX, 這個內(nèi)容就是我們配置的文件秧骑。再仔細(xì)的跟進(jìn)代碼版确,我們會進(jìn)入到parse()方法扣囊,該方法用于按照行讀取出文件中的內(nèi)容,并保存到Iterator<String>
中绒疗。
故而侵歇,再通過 nextName = pending.next();
執(zhí)行后,獲取到top.flygrk.ishare.spi.service.impl.ASPIServiceImpl
吓蘑,繼而進(jìn)行后續(xù)的next()方法操作惕虑。
然后進(jìn)入到nextService()方法:
再nextService()方法里,使用了反射的技術(shù)磨镶,根據(jù)前面從文件中讀取到的實現(xiàn)類全路徑top.flygrk.ishare.spi.service.impl.ASPIServiceImpl
獲取到該實現(xiàn)類的對象溃蔫!走到這里,也就基本上了解了SPI琳猫,但是我們能只獲取ASPIServiceImpl伟叛,而不去獲取BSPIServiceImpl嗎?對不起脐嫂,這里不允許這樣统刮,只能通過迭代器遍歷出所有的內(nèi)容!除非人為干預(yù)(外層循環(huán)比對完成之后退出循環(huán))账千。接下來的步驟就和前面幾乎一致了侥蒙,這里不再細(xì)述~
5 SPI 優(yōu)缺點
我們評價一門思想往往需要從其優(yōu)缺點的方向進(jìn)行考慮。SPI同樣也是有一定的優(yōu)缺點存在的匀奏,下面我們來仔細(xì)的看下它有哪些優(yōu)缺點:
5.1 優(yōu)點
- 解耦:最大的優(yōu)點也就是解耦了辉哥,通過SPI可以使第三方服務(wù)模塊的邏輯與業(yè)務(wù)代碼相分離,而不耦合在一起攒射。應(yīng)用程序可以根據(jù)實際業(yè)務(wù)進(jìn)行擴(kuò)展。
5.2 缺點
參考dubbo官方文檔
- 需要遍歷所有的實現(xiàn)恒水,并實例化会放,然后我們在循環(huán)中才能找到我們需要的實現(xiàn)。
- 配置文件中只是簡單的列出了所有的擴(kuò)展實現(xiàn)钉凌,而沒有給他們命名咧最。導(dǎo)致在程序中很難去準(zhǔn)確的引用它們。
- 擴(kuò)展如果依賴其他的擴(kuò)展御雕,做不到自動注入和裝配
- 不提供類似于Spring的IOC和AOP功能
- 擴(kuò)展很難和其他的框架集成矢沿,比如擴(kuò)展里面依賴了一個Spring bean,原生的Java SPI不支持
6 SPI案例分析
在我們常用的框架中酸纲,有很多都是有使用SPI的方式捣鲸,其中包括JDBC加載不同類型數(shù)據(jù)庫的驅(qū)動、SLF4J加載不同提供商的日志實現(xiàn)類闽坡、Spring 框架栽惶、Dubbo框架愁溜。
這里需要注意,dubbo框架的SPI是對原生的Java SPI 進(jìn)行了擴(kuò)展的外厂。關(guān)于dubbo的SPI我們將在后面詳細(xì)講解∶嵯螅現(xiàn)在,我們來以JDBC加載的方式來簡單的看看其SPI的方式汁蝶。
我們先找到mysql的包渐扮,其結(jié)構(gòu)如下:
在META-INF/services 目錄下,存在 文件 java.sql.Driver掖棉,其內(nèi)容為:
通過這個路徑墓律,我們也可以找到 com.mysql.jdbc.Driver類,它實現(xiàn)了java.sql.Driver接口:
諸如Oracle啊片,同樣也有此機(jī)制只锻,這里就不再細(xì)述了,請自行驗證查看~
Blog:
- 簡書: http://www.reibang.com/u/91378a397ffe
- csdn: https://blog.csdn.net/ZhiyouWu
- 開源中國: https://my.oschina.net/u/3204088
- 掘金: https://juejin.im/user/5b5979efe51d451949094265
- 博客園: https://www.cnblogs.com/zhiyouwu/
- 微信公眾號: 源碼灣
- 微信: WZY1782357529 (歡迎溝通交流)