對Java中SPI的理解

什么是SPI

SPI全稱Service Provider Interface,字面意思是提供服務(wù)的接口,再解釋詳細(xì)一下就是Java提供的一套用來被第三方實(shí)現(xiàn)或擴(kuò)展的接口,實(shí)現(xiàn)了接口的動態(tài)擴(kuò)展,讓第三方的實(shí)現(xiàn)類能像插件一樣嵌入到系統(tǒng)中。

咦。。。

這個(gè)解釋感覺還是有點(diǎn)繞口。

那就說一下它的本質(zhì)。

將接口的實(shí)現(xiàn)類的全限定名配置在文件中(文件名是接口的全限定名),由服務(wù)加載器讀取配置文件,加載實(shí)現(xiàn)類。實(shí)現(xiàn)了運(yùn)行時(shí)動態(tài)為接口替換實(shí)現(xiàn)類。

SPI示例

還是舉例說明吧。

我們創(chuàng)建一個(gè)項(xiàng)目,然后創(chuàng)建一個(gè)module叫spi-interface。

在這個(gè)module中我們定義一個(gè)接口:

/**

* @author jimoer

**/publicinterfaceSpiInterfaceService{/**

? ? * 打印參數(shù)

? ? * @param parameter 參數(shù)

? ? */voidprintParameter(String parameter);}

再定義一個(gè)module,名字叫spi-service-one,pom.xml中依賴spi-interface。

在spi-service-one中定義一個(gè)實(shí)現(xiàn)類,實(shí)現(xiàn)SpiInterfaceService 接口。

packagecom.jimoer.spi.service.one;importcom.jimoer.spi.app.SpiInterfaceService;/** *@authorjimoer **/publicclassSpiOneServiceimplementsSpiInterfaceService{/**? ? * 打印參數(shù)? ? *? ? *@paramparameter 參數(shù)? ? */@OverridepublicvoidprintParameter(String parameter){? ? ? ? System.out.println("我是SpiOneService:"+parameter);? ? }}

然后在spi-service-one的resources目錄下創(chuàng)建目錄META-INF/services,在此目錄下創(chuàng)建一個(gè)文件名稱為SpiInterfaceService接口的全限定名稱,文件內(nèi)容寫入SpiOneService這個(gè)實(shí)現(xiàn)類的全限定名稱。

效果如下:

再創(chuàng)建一個(gè)module,名稱為:spi-service-one,也是依賴spi-interface,并且定義一個(gè)實(shí)現(xiàn)類SpiTwoService 來實(shí)現(xiàn)SpiInterfaceService 接口。

packagecom.jimoer.spi.service.two;importcom.jimoer.spi.app.SpiInterfaceService;/** *@authorjimoer **/publicclassSpiTwoServiceimplementsSpiInterfaceService{/**? ? * 打印參數(shù)? ? *? ? *@paramparameter 參數(shù)? ? */@OverridepublicvoidprintParameter(String parameter){? ? ? ? System.out.println("我是SpiTwoService:"+parameter);? ? }}

目錄結(jié)構(gòu)如下:

下面再創(chuàng)建一個(gè)用來測試的module,名為:spi-app。

pom.xml中依賴spi-service-one和spi-service-two

com.jimoer.spispi-service-one1.0-SNAPSHOTcom.jimoer.spispi-service-two1.0-SNAPSHOT

創(chuàng)建測試類

/** *@authorjimoer **/publicclassSpiService{publicstaticvoidmain(String[] args){? ? ? ? ServiceLoader spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class);? ? ? ? Iterator iterator = spiInterfaceServices.iterator();while(iterator.hasNext()){? ? ? ? ? ? SpiInterfaceService sip = iterator.next();? ? ? ? ? ? sip.printParameter("參數(shù)");? ? ? ? }? ? }}

執(zhí)行結(jié)果:

我是SpiTwoService:參數(shù)我是SpiOneService:參數(shù)

通過運(yùn)行結(jié)果我們可以看到,已經(jīng)將SpiInterfaceService接口的所有實(shí)現(xiàn)都加載到了當(dāng)前項(xiàng)目中,并且執(zhí)行了調(diào)用。

這整個(gè)代碼結(jié)構(gòu)我們可以看出SPI機(jī)制將模塊的裝配放到了程序外面,就是說,接口的實(shí)現(xiàn)可以在程序外面,只需要在使用的時(shí)候指定具體的實(shí)現(xiàn)。并且動態(tài)的加載到自己的項(xiàng)目中。

SPI機(jī)制的主要目的:

一是為了解耦,將接口和具體實(shí)現(xiàn)分離開來;

二是提高框架的擴(kuò)展性。以前寫程序的時(shí)候,接口和實(shí)現(xiàn)都寫在一起,調(diào)用方在使用的時(shí)候依賴接口來進(jìn)行調(diào)用,無權(quán)選擇使用具體的實(shí)現(xiàn)類。

SPI的實(shí)現(xiàn)

那么我們來看一下SPI具體是如何實(shí)現(xiàn)的呢?

通過上面的例子,我們可以看到,SPI機(jī)制的核心代碼是下面這段:

ServiceLoader spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class);

那么我們來看一下ServiceLoader.load()方法的源碼:

publicstatic ServiceLoader load(Classservice){? ? ClassLoader cl = Thread.currentThread().getContextClassLoader();returnServiceLoader.load(service, cl);}

看到Thread.currentThread().getContextClassLoader();我就明白是怎么回事了,這個(gè)就是線程上下文類加載器,因?yàn)?b>線程上下文類加載器就是為了做類加載雙親委派模型的逆序而創(chuàng)建的。

使用這個(gè)線程上下文類加載器去加載所需的SPI服務(wù)代碼,這是一種父類加載器去請求子類加載器完成類加載的行為,這種行為實(shí)際上是打通了,雙親委派模型的層次結(jié)構(gòu)來逆向使用類加載器,已經(jīng)違背了雙親委派模型的一般性原則,但也是無可奈何的事情。

《深入理解Java虛擬機(jī)(第三版)》

雖然知道了它是破壞雙親委派的了,但是具體實(shí)現(xiàn),還是需要具體往下看的。

在ServiceLoader里找到具體實(shí)現(xiàn)hasNext()的方法了,那么繼續(xù)來看這個(gè)方法的實(shí)現(xiàn)。

hasNext()方法又主要調(diào)用了hasNextService()方法。

// 固定路徑privatestaticfinalString PREFIX ="META-INF/services/";privatebooleanhasNextService(){if(nextName !=null) {returntrue;? ? }if(configs ==null) {try{// 固定路徑+接口全限定名稱String fullName = PREFIX + service.getName();// 如果當(dāng)前線程上下文類加載器為空,會用父類加載器(默認(rèn)是應(yīng)用程序類加載器)if(loader ==null)? ? ? ? ? ? ? ? configs = ClassLoader.getSystemResources(fullName);elseconfigs = loader.getResources(fullName);? ? ? ? }catch(IOException x) {? ? ? ? ? ? fail(service,"Error locating configuration files", x);? ? ? ? }? ? }while((pending ==null) || !pending.hasNext()) {if(!configs.hasMoreElements()) {returnfalse;? ? ? ? }? ? ? ? pending = parse(service, configs.nextElement());? ? }// 后面next()方法中判斷當(dāng)前類是否已經(jīng)出現(xiàn)化的時(shí)候要用nextName = pending.next();returntrue; }

主要就是去加載META-INF/services/路徑下的接口全限定名稱的文件然后去里面找到實(shí)現(xiàn)類的類路徑將實(shí)現(xiàn)類進(jìn)行類加載。

繼續(xù)看迭代器是如何取出每一個(gè)實(shí)現(xiàn)對象的。那就要看ServiceLoader中實(shí)現(xiàn)了迭代器的next()方法了。

next()方法主要是nextService()實(shí)現(xiàn)的,那么繼續(xù)看nextService()方法。

privateS nextService() {if(!hasNextService())thrownewNoSuchElementException();Stringcn = nextName;? ? nextName =null;? ? Class c =null;try{// 直接加載類,無需初始化(因?yàn)樯厦鎕asNext()已經(jīng)初始化了)。c = Class.forName(cn,false, loader);? ? }catch(ClassNotFoundException x) {? ? ? ? fail(service,"Provider "+ cn +" not found");? ? }if(!service.isAssignableFrom(c)) {? ? ? ? fail(service,"Provider "+ cn? +" not a subtype");? ? }try{// 將加載好的類實(shí)例化出對象。S p = service.cast(c.newInstance());? ? ? ? providers.put(cn, p);returnp;? ? }catch(Throwable x) {? ? ? ? fail(service,"Provider "+ cn +" could not be instantiated",? ? ? ? ? ? ? x);? ? }thrownewError();// This cannot happen}

看到這里就可以明白了,是如何創(chuàng)建出對象的了。先在hasNext()將接口的實(shí)現(xiàn)類進(jìn)行加載并判斷是否存在接口的實(shí)現(xiàn)類,然后在next()方法中將實(shí)現(xiàn)類進(jìn)實(shí)例化。

Java中使用SPI機(jī)制的功能其實(shí)有很多,像JDBC、JNDI、以及Spring中也有使用,甚至RPC框架(Dubbo)中也有使用SPI機(jī)制來實(shí)現(xiàn)功能。

作者:紀(jì)莫

鏈接:https://www.cnblogs.com/jimoer/p/14095489.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容