Java中SPI的一些理解
前言
最近在面試的時(shí)候被問(wèn)到SPI了,沒(méi)回答上來(lái),主要也是自己的原因,把自己給帶溝里去了,因?yàn)橹v到了類(lèi)加載器的雙親委派模型,后面就被問(wèn)到了有哪些是破壞了雙親委派模型的場(chǎng)景,然后我就說(shuō)到了SPI,JNDI,以及JDK9的模塊化都破壞了雙親委派。
然后就被問(wèn),那你說(shuō)說(shuō)對(duì)Java中的SPI的理解吧。然后我就一臉懵逼了,之前只是知道它會(huì)破壞雙親委派,也知道是個(gè)怎么回事,但是并沒(méi)有深入了解,那么這次我就好好的來(lái)總結(jié)一下這個(gè)知識(shí)吧。
什么是SPI
SPI全稱(chēng)Service Provider Interface,字面意思是提供服務(wù)的接口,再解釋詳細(xì)一下就是Java提供的一套用來(lái)被第三方實(shí)現(xiàn)或擴(kuò)展的接口,實(shí)現(xiàn)了接口的動(dòng)態(tài)擴(kuò)展,讓第三方的實(shí)現(xiàn)類(lèi)能像插件一樣嵌入到系統(tǒng)中。
咦。。。
這個(gè)解釋感覺(jué)還是有點(diǎn)繞口。
那就說(shuō)一下它的本質(zhì)。
將接口的實(shí)現(xiàn)類(lèi)的全限定名配置在文件中(文件名是接口的全限定名),由服務(wù)加載器讀取配置文件,加載實(shí)現(xiàn)類(lèi)。實(shí)現(xiàn)了運(yùn)行時(shí)動(dòng)態(tài)為接口替換實(shí)現(xiàn)類(lèi)。
SPI示例
還是舉例說(shuō)明吧。
我們創(chuàng)建一個(gè)項(xiàng)目,然后創(chuàng)建一個(gè)module叫spi-interface。
在這個(gè)module中我們定義一個(gè)接口:
/** * @author jimoer **/ public interface SpiInterfaceService { /** * 打印參數(shù) * @param parameter 參數(shù) */ void printParameter(String parameter); }
再定義一個(gè)module,名字叫spi-service-one,pom.xml中依賴(lài)spi-interface。
在spi-service-one中定義一個(gè)實(shí)現(xiàn)類(lèi),實(shí)現(xiàn)SpiInterfaceService 接口。
package com.jimoer.spi.service.one; import com.jimoer.spi.app.SpiInterfaceService; /** * @author jimoer **/ public class SpiOneService implements SpiInterfaceService { /** * 打印參數(shù) * * @param parameter 參數(shù) */ @Override public void printParameter(String parameter) { System.out.println("我是SpiOneService:"+parameter); } }
然后再spi-service-one的resources目錄下創(chuàng)建目錄META-INF/services,在此目錄下創(chuàng)建一個(gè)文件名稱(chēng)為SpiInterfaceService接口的全限定名稱(chēng),文件內(nèi)容寫(xiě)入SpiOneService這個(gè)實(shí)現(xiàn)類(lèi)的全限定名稱(chēng)。
效果如下:
再創(chuàng)建一個(gè)module,名稱(chēng)為:spi-service-one,也是依賴(lài)spi-interface,并且定義一個(gè)實(shí)現(xiàn)類(lèi)SpiTwoService 來(lái)實(shí)現(xiàn)SpiInterfaceService 接口。
package com.jimoer.spi.service.two; import com.jimoer.spi.app.SpiInterfaceService; /** * @author jimoer **/ public class SpiTwoService implements SpiInterfaceService { /** * 打印參數(shù) * * @param parameter 參數(shù) */ @Override public void printParameter(String parameter) { System.out.println("我是SpiTwoService:"+parameter); } }
目錄結(jié)構(gòu)如下:
下面再創(chuàng)建一個(gè)用來(lái)測(cè)試的module,名為:spi-app。
pom.xml中依賴(lài)spi-service-one
和spi-service-two
<dependencies> <dependency> <groupId>com.jimoer.spi</groupId> <artifactId>spi-service-one</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.jimoer.spi</groupId> <artifactId>spi-service-two</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>
創(chuàng)建測(cè)試類(lèi)
/** * @author jimoer **/ public class SpiService { public static void main(String[] args) { ServiceLoader<SpiInterfaceService> spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class); Iterator<SpiInterfaceService> iterator = spiInterfaceServices.iterator(); while (iterator.hasNext()){ SpiInterfaceService sip = iterator.next(); sip.printParameter("參數(shù)"); } } }
執(zhí)行結(jié)果:
我是SpiTwoService:參數(shù)
我是SpiOneService:參數(shù)
通過(guò)運(yùn)行結(jié)果我們可以看到,已經(jīng)將SpiInterfaceService接口的所有實(shí)現(xiàn)都加載到了當(dāng)前項(xiàng)目中,并且執(zhí)行了調(diào)用。
這整個(gè)代碼結(jié)構(gòu)我們可以看出SPI機(jī)制將模塊的裝配放到了程序外面,就是說(shuō),接口的實(shí)現(xiàn)可以在程序外面,只需要在使用的時(shí)候指定具體的實(shí)現(xiàn)。并且動(dòng)態(tài)的加載到自己的項(xiàng)目中。
SPI機(jī)制的主要目的:
一是為了解耦,將接口和具體實(shí)現(xiàn)分離開(kāi)來(lái);
二是提高框架的擴(kuò)展性。以前寫(xiě)程序的時(shí)候,接口和實(shí)現(xiàn)都寫(xiě)在一起,調(diào)用方在使用的時(shí)候依賴(lài)接口來(lái)進(jìn)行調(diào)用,無(wú)權(quán)選擇使用具體的實(shí)現(xiàn)類(lèi)。
SPI的實(shí)現(xiàn)
那么我們來(lái)看一下SPI具體是如何實(shí)現(xiàn)的呢?
通過(guò)上面的例子,我們可以看到,SPI機(jī)制的核心代碼是下面這段:
ServiceLoader<SpiInterfaceService> spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class);
那么我們來(lái)看一下ServiceLoader.load()
方法的源碼:
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
看到Thread.currentThread().getContextClassLoader();我就明白是怎么回事了,這個(gè)就是線程上下文類(lèi)加載器,因?yàn)榫€程上下文類(lèi)加載器就是為了做類(lèi)加載雙親委派模型的逆序而創(chuàng)建的。
使用這個(gè)線程上下文類(lèi)加載器去加載所需的SPI服務(wù)代碼,這是一種父類(lèi)加載器去請(qǐng)求子類(lèi)加載器完成類(lèi)加載的行為,這種行為實(shí)際上是打通了,雙親委派模型的層次結(jié)構(gòu)來(lái)逆向使用類(lèi)加載器,已經(jīng)違背了雙親委派模型的一般性原則,但也是無(wú)可奈何的事情。
《深入理解Java虛擬機(jī)(第三版)》
雖然知道了它是破壞雙親委派的了,但是具體實(shí)現(xiàn),還是需要具體往下看的。
在ServiceLoader里找到具體實(shí)現(xiàn)hasNext()的方法了,那么繼續(xù)來(lái)看這個(gè)方法的實(shí)現(xiàn)。
hasNext()方法又主要調(diào)用了hasNextService()方法。
// 固定路徑 private static final String PREFIX = "META-INF/services/"; private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { // 固定路徑+接口全限定名稱(chēng) String fullName = PREFIX + service.getName(); // 如果當(dāng)前線程上下文類(lèi)加載器為空,會(huì)用父類(lèi)加載器(默認(rèn)是應(yīng)用程序類(lèi)加載器) if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } // 后面next()方法中判斷當(dāng)前類(lèi)是否已經(jīng)出現(xiàn)化的時(shí)候要用 nextName = pending.next(); return true; }
主要就是去加載META-INF/services/路徑下的接口全限定名稱(chēng)的文件然后去里面找到實(shí)現(xiàn)類(lèi)的類(lèi)路徑將實(shí)現(xiàn)類(lèi)進(jìn)行類(lèi)加載。
繼續(xù)看迭代器是如何取出每一個(gè)實(shí)現(xiàn)對(duì)象的。那就要看ServiceLoader中實(shí)現(xiàn)了迭代器的next()方法了。
next()方法主要是nextService()實(shí)現(xiàn)的,那么繼續(xù)看nextService()方法。
private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { // 直接加載類(lèi),無(wú)需初始化(因?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 { // 將加載好的類(lèi)實(shí)例化出對(duì)象。 S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }
看到這里就可以明白了,是如何創(chuàng)建出對(duì)象的了。先在hasNext()將接口的實(shí)現(xiàn)類(lèi)進(jìn)行加載并判斷是否存在接口的實(shí)現(xiàn)類(lèi),然后在next()方法中將實(shí)現(xiàn)類(lèi)進(jìn)實(shí)例化。
Java中使用SPI機(jī)制的功能其實(shí)有很多,像JDBC、JNDI、以及Spring中也有使用,甚至RPC框架(Dubbo)中也有使用SPI機(jī)制來(lái)實(shí)現(xiàn)功能。
以上就是Java中SPI的一些理解的詳細(xì)內(nèi)容,更多關(guān)于Java SPI的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IntelliJ IDEA 2020.2正式發(fā)布,兩點(diǎn)多多總能助你提效
這篇文章主要介紹了IntelliJ IDEA 2020.2正式發(fā)布,諸多亮點(diǎn)總有幾款能助你提效,本文通過(guò)圖文實(shí)例代碼相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2020-07-07SpringBoot整合Springsecurity實(shí)現(xiàn)數(shù)據(jù)庫(kù)登錄及權(quán)限控制功能
本教程詳細(xì)介紹了如何使用SpringBoot整合SpringSecurity實(shí)現(xiàn)數(shù)據(jù)庫(kù)登錄和權(quán)限控制,本文分步驟結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-10-10springboot 返回json格式數(shù)據(jù)時(shí)間格式配置方式
這篇文章主要介紹了springboot 返回json格式數(shù)據(jù)時(shí)間格式配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Java LocalCache 本地緩存的實(shí)現(xiàn)實(shí)例
本篇文章主要介紹了Java LocalCache 本地緩存的實(shí)現(xiàn)實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-05-05springboot tomcat的maxHttpFormPostSize參數(shù)示例解析
這篇文章主要介紹了springboot tomcat的maxHttpFormPostSize參數(shù)示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08Java中雙重檢查鎖(double checked locking)的正確實(shí)現(xiàn)
雙重檢查鎖(Double-Check Locking),顧名思義,通過(guò)兩次檢查,并基于加鎖機(jī)制,實(shí)現(xiàn)某個(gè)功能,下面這篇文章主要給大家介紹了關(guān)于Java中雙重檢查鎖(double checked locking)的相關(guān)資料,需要的朋友可以參考下2021-09-09