你說說對Java中SPI的理解吧

紀莫發表於2020-12-07

前言

最近在面試的時候被問到SPI了,沒回答上來,主要也是自己的原因,把自己給帶溝裡去了,因為講到了類載入器的雙親委派模型,後面就被問到了有哪些是破壞了雙親委派模型的場景,然後我就說到了SPI,JNDI,以及JDK9的模組化都破壞了雙親委派。
然後就被問,那你說說對Java中的SPI的理解吧。然後我就一臉懵逼了,之前只是知道它會破壞雙親委派,也知道是個怎麼回事,但是並沒有深入瞭解,那麼這次我就好好的來總結一下這個知識吧。

什麼是SPI

SPI全稱Service Provider Interface,字面意思是提供服務的介面,再解釋詳細一下就是Java提供的一套用來被第三方實現或擴充套件的介面,實現了介面的動態擴充套件,讓第三方的實現類能像外掛一樣嵌入到系統中。

咦。。。
這個解釋感覺還是有點繞口。
那就說一下它的本質。

將介面的實現類的全限定名配置在檔案中(檔名是介面的全限定名),由服務載入器讀取配置檔案,載入實現類。實現了執行時動態為介面替換實現類。

SPI示例

還是舉例說明吧。
我們建立一個專案,然後建立一個module叫spi-interface。
在這裡插入圖片描述
在這個module中我們定義一個介面:

/**
 * @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-onespi-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("引數");
        }
    }
}

執行結果:

我是SpiTwoService:引數
我是SpiOneService:引數

通過執行結果我們可以看到,已經將SpiInterfaceService介面的所有實現都載入到了當前專案中,並且執行了呼叫。
在這裡插入圖片描述
這整個程式碼結構我們可以看出SPI機制將模組的裝配放到了程式外面,就是說,介面的實現可以在程式外面,只需要在使用的時候指定具體的實現。並且動態的載入到自己的專案中。
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機制來實現功能。

相關文章