1. 程式人生 > 實用技巧 >JDK中的SPI機制

JDK中的SPI機制

前言

最近學習類載入的過程中,瞭解到JDK提供給我們的一個可擴充套件的介面:java.util.ServiceLoader
之前自己不瞭解這個機制,甚是慚愧...

什麼是SPI

SPI全稱為(Service Provider Interface),是JDK內建的一種服務提供發現機制。SPI是一種動態替換髮現的機制,比如有個介面,想執行時動態的給它新增實現,你只需要新增一個實現。我們經常遇到的就是java.sql.Driver介面,其他不同廠商可以針對同一介面做出不同的實現,mysql和postgresql都有不同的實現提供給使用者,而Java的SPI機制可以為某個介面尋找服務實現。

首先放個圖:我們在“呼叫方”和“實現方”之間需要引入“介面”,可以思考一下什麼情況應該把介面放入呼叫方,什麼時候可以把介面歸為實現方。

先來看看介面屬於實現方的情況,這個很容易理解,實現方提供了介面和實現,我們可以引用介面來達到呼叫某實現類的功能,這就是我們經常說的api,它具有以下特徵:
1.是對實現的說明(我可以給你提供什麼)
2.組織上位於實現方所在的包中
3.實現和介面在一個包中

當介面屬於呼叫方時,我們就將其稱為spi,全稱為:service provider interface,spi的規則如下:
1.是對實現的約束(要提供這個功能,實現者需要做那些事情)
2.組織上位於呼叫方所在的包中
3.實現位於獨立的包中(也可認為在提供方中)

簡而言之
API會告訴您特定的類/方法為您執行什麼操作,而SPI則告訴您必須執行哪些操作才能符合要求。通常,API和SPI是分開的。例如,在JDBC中,Driver類是SPI的一部分:如果只想使用JDBC,則不需要直接使用它,但是實現JDBC驅動程式的每個人都必須實現該類。但是,有時它們會重疊

。Connection介面既是SPI,又是API:您在使用JDBC驅動程式時通常會使用它,並且需要由JDBC驅動程式的開發人員來實現。

JDK SPI使用說明及示例

要使用SPI比較簡單,只需要按照以下幾個步驟操作即可:

1.在jar包的META-INF/services目錄下建立一個以"介面全限定名"為命名的檔案,內容為實現類的全限定名
2.介面實現類所在的jar包在classpath下
3.主程式通過java.util.ServiceLoader動態狀態實現模組,它通過掃描META-INF/services目錄下的配置檔案找到實現類的全限定名,把類載入到JVM
4.SPI的實現類必須帶一個無參構造方法

舉例1

下例是使用maven引入了mysql的依賴後執行的,MySQL驅動內的截圖:

java.sql.Driver檔案全部內容:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
public class SpiTest {

    public static void main(String[] args) {
        ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
        Iterator<Driver> iterator = loader.iterator();
        while(iterator.hasNext()) {
            Driver driver = iterator.next();
            System.out.println("driver is " + driver.getClass() + ", classLoader is " + driver.getClass().getClassLoader());
        }
        System.out.println("當前上下文類載入器是:" + Thread.currentThread().getContextClassLoader());
        System.out.println("ServiceLoader的類載入器是:" + ServiceLoader.class.getClassLoader());
    }
}

執行結果:

driver is class com.mysql.jdbc.Driver, classLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
driver is class com.mysql.fabric.jdbc.FabricMySQLDriver, classLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
當前上下文類載入器是:sun.misc.Launcher$AppClassLoader@18b4aac2
ServiceLoader的類載入器是:null

SPI相關的類載入的邏輯

因為ServiceLoader位於java.util.ServiceLoader,所以這個類是會被啟動類載入器所載入,然後我們分析一下ServiceLoader.load(Driver.class)的原始碼。
根據類載入的原理:如果一個類由類載入器A載入,那麼這個類的依賴類也會被類載入器A載入(前提是這個依賴類尚未被載入過)。
當執行ServiceLoader.load(Driver.class),如果不使用執行緒上下文類載入器來打破雙親委託模型,那麼該方法的關聯類也會被啟動類載入器載入。

333    public static <S> ServiceLoader<S> load(Class<S> service) {
334        ClassLoader cl = Thread.currentThread().getContextClassLoader();
335        return ServiceLoader.load(service, cl);
336    }

所以我們看到334行獲取了執行緒上下文類載入器,然後呼叫另一個過載的load方法去載入Driver(即所謂的Service)。
繼續跟蹤ServiceLoader.load(service, cl)的程式碼,會發現它依次執行了如下動作:
1.初始化了一個:ServiceLoader物件,new ServiceLoader<>(service, loader)
2.執行reload()
3.new LazyIterator(service, loader)
這裡所有的loader都是執行緒上下文類載入器,它預設為系統類載入器。

這時,SpiTest中的ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);就執行完了,
跟著執行SpiTest中的Iterator<Driver> iterator = loader.iterator();
這裡當執行iterator.hasNext()的時候,就會進入到剛才初始化的LazyIterator類中,執行其中的下列方法,
所以這也是為什麼"在jar包的META-INF/services目錄下建立一個以"介面全限定名"為命名的檔案,內容為實現類的全限定名"

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    //這裡的PREFIX是一個常量:META-INF/services/
                    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());
            }
            nextName = pending.next();
            return true;
        }

仔細閱讀內部類LazyIterator類的原始碼,就可以知道:
1.JDK是怎麼讀取META-INF/services目錄下的內容
2.JDK是怎麼載入這些SPI的類的