最近在面試的時候被問到SPI了,沒回答上來,主要也是本身的緣由,把本身給帶溝裏去了,由於講到了類加載器的雙親委派模型,後面就被問到了有哪些是破壞了雙親委派模型的場景,而後我就說到了SPI,JNDI,以及JDK9的模塊化都破壞了雙親委派。
而後就被問,那你說說對Java中的SPI的理解吧。而後我就一臉懵逼了,以前只是知道它會破壞雙親委派,也知道是個怎麼回事,可是並無深刻了解,那麼此次我就好好的來總結一下這個知識吧。java
SPI全稱Service Provider Interface,字面意思是提供服務的接口,再解釋詳細一下就是Java提供的一套用來被第三方實現或擴展的接口,實現了接口的動態擴展,讓第三方的實現類能像插件同樣嵌入到系統中。面試
咦。。。
這個解釋感受仍是有點繞口。
那就說一下它的本質。app
將接口的實現類的全限定名配置在文件中(文件名是接口的全限定名),由服務加載器讀取配置文件,加載實現類。實現了運行時動態爲接口替換實現類。框架
仍是舉例說明吧。
咱們建立一個項目,而後建立一個module叫spi-interface。
在這個module中咱們定義一個接口:ide
/** * @author jimoer **/ public interface SpiInterfaceService { /** * 打印參數 * @param parameter 參數 */ void printParameter(String parameter); }
再定義一個module,名字叫spi-service-one,pom.xml中依賴spi-interface。
在spi-service-one中定義一個實現類,實現SpiInterfaceService 接口。模塊化
package com.jimoer.spi.service.one; import com.jimoer.spi.app.SpiInterfaceService; /** * @author jimoer **/ public class SpiOneService implements SpiInterfaceService { /** * 打印參數 * * @param parameter 參數 */ @Override public void printParameter(String parameter) { System.out.println("我是SpiOneService:"+parameter); } }
而後再spi-service-one的resources目錄下建立目錄META-INF/services,在此目錄下建立一個文件名稱爲SpiInterfaceService接口的全限定名稱,文件內容寫入SpiOneService這個實現類的全限定名稱。
效果以下:
再建立一個module,名稱爲:spi-service-one,也是依賴spi-interface,而且定義一個實現類SpiTwoService 來實現SpiInterfaceService 接口。測試
package com.jimoer.spi.service.two; import com.jimoer.spi.app.SpiInterfaceService; /** * @author jimoer **/ public class SpiTwoService implements SpiInterfaceService { /** * 打印參數 * * @param parameter 參數 */ @Override public void printParameter(String parameter) { System.out.println("我是SpiTwoService:"+parameter); } }
目錄結構以下:
下面再建立一個用來測試的module,名爲:spi-app。
pom.xml中依賴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>
建立測試類線程
/** * @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("參數"); } } }
執行結果:3d
我是SpiTwoService:參數 我是SpiOneService:參數
經過運行結果咱們能夠看到,已經將SpiInterfaceService接口的全部實現都加載到了當前項目中,而且執行了調用。
這整個代碼結構咱們能夠看出SPI機制將模塊的裝配放到了程序外面,就是說,接口的實現能夠在程序外面,只須要在使用的時候指定具體的實現。而且動態的加載到本身的項目中。
SPI機制的主要目的:
一是爲了解耦,將接口和具體實現分離開來;
二是提升框架的擴展性。之前寫程序的時候,接口和實現都寫在一塊兒,調用方在使用的時候依賴接口來進行調用,無權選擇使用具體的實現類。
那麼咱們來看一下SPI具體是如何實現的呢?
經過上面的例子,咱們能夠看到,SPI機制的核心代碼是下面這段:
ServiceLoader<SpiInterfaceService> spiInterfaceServices = ServiceLoader.load(SpiInterfaceService.class);
那麼咱們來看一下ServiceLoader.load()
方法的源碼:
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
看到Thread.currentThread().getContextClassLoader()
;我就明白是怎麼回事了,這個就是線程上下文類加載器,由於線程上下文類加載器就是爲了作類加載雙親委派模型的逆序而建立的。
使用這個線程上下文類加載器去加載所需的SPI服務代碼,這是一種父類加載器去請求子類加載器完成類加載的行爲,這種行爲其實是打通了,雙親委派模型的層次結構來逆向使用類加載器,已經違背了雙親委派模型的通常性原則,但也是迫不得已的事情。
《深刻理解Java虛擬機(第三版)》
雖然知道了它是破壞雙親委派的了,可是具體實現,仍是須要具體往下看的。
在ServiceLoader裏找到具體實現hasNext()的方法了,那麼繼續來看這個方法的實現。
hasNext()方法又主要調用了hasNextService()方法。
// 固定路徑 private static final String PREFIX = "META-INF/services/"; private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { // 固定路徑+接口全限定名稱 String fullName = PREFIX + service.getName(); // 若是當前線程上下文類加載器爲空,會用父類加載器(默認是應用程序類加載器) 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()方法中判斷當前類是否已經出現化的時候要用 nextName = pending.next(); return true; }
主要就是去加載META-INF/services/路徑下的接口全限定名稱的文件而後去裏面找到實現類的類路徑將實現類進行類加載。
繼續看迭代器是如何取出每個實現對象的。那就要看ServiceLoader中實現了迭代器的next()方法了。
next()方法主要是nextService()實現的,那麼繼續看nextService()方法。
private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { // 直接加載類,無需初始化(由於上面hasNext()已經初始化了)。 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 { // 將加載好的類實例化出對象。 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 }
看到這裏就能夠明白了,是如何建立出對象的了。先在hasNext()將接口的實現類進行加載並判斷是否存在接口的實現類,而後在next()方法中將實現類進實例化。
Java中使用SPI機制的功能其實有不少,像JDBC、JNDI、以及Spring中也有使用,甚至RPC框架(Dubbo)中也有使用SPI機制來實現功能。