Java 的 SPI 機制

VipSoft發表於2023-04-20

什麼是SPI機制?

SPI機制( Service Provider Interface)是Java的一種服務發現機制,為了方便應用擴充套件。那什麼是服務發現機制?簡單來說,就是你定義了一個介面,但是不提供實現,介面實現由其他系統應用實現。你只需要提供一種可以找到其他系統提供的介面實現類的能力或者說機制.
SPI機制在Java中有很廣泛的運用,比如:eclipse和idea裡的外掛使用就是透過SPI機制實現的。開發工具提供一個擴充套件介面,具體的實現由外掛開發者實現,開發工具提供一種服務發現機制來找到具體外掛的實現,這就達到了外掛的安裝效果。從而可以使用外掛服務。如果不需要某一外掛,只需要刪除某一外掛的實現類,開發工具找不到具體的外掛實現,這就達到了外掛的解除安裝效果。不管是安裝還是解除安裝都不會影響其他程式碼,其他服務。非常方便的實現了可插拔的效果。

JDBC中資料庫連線驅動也使用了SPI機制,來達到適配不同DB資料庫的效果。

SPI機制除了在jdk裡有運用,在springboot中也用到了。springboot自動裝配中"查詢spring.factories 檔案步驟"就是基於SPI的部分設計思想實現的。

SPI 有如下的好處:

不需要改動原始碼就可以實現擴充套件,解耦。
實現擴充套件對原來的程式碼幾乎沒有侵入性。
只需要新增配置就可以實現擴充套件,符合開閉原則。

API 和 SPI 區別

API:大多數情況下,都是實現方制定介面並完成對介面的實現,呼叫方僅僅依賴介面呼叫。
SPI :是呼叫方來制定介面規範,提供給外部來實現,呼叫方在呼叫時則選擇自己需要的外部實現。
image

SPI實現服務介面與服務實現的解耦:

  • 服務提供者(如 springboot starter)提供出 SPI 介面,讓客戶端去自定義實現。
  • 客戶端(普通的 springboot 專案)即可透過本地註冊的形式,將實現類註冊到服務端,輕鬆實現可插拔。
    image

簡單實現

定義介面

package com.test.service;

public interface ISpi {
    void say();
}

第一個實現類:

package com.test.service.impl;

import com.test.service.ISpi;

public class FirstSpiImpl implements ISpi {

    @Override
    public void say() {
        System.out.println("我是第一個SPI實現類");
    }
}

第二個實現類:

package com.test.service.impl;

import com.test.service.ISpi;

public class SecondSpiImpl implements ISpi {

    @Override
    public void say() {
        System.out.println("我是第二個SPI實現類");
    }
}

在resources目錄下新建META-INF/services目錄,並且在這個目錄下新建一個與上述介面的全限定名一致的檔案,在這個檔案中寫入介面的實現類的全限定名,並寫上需要動態載入的實現類的全路徑名。

#com.test.service.impl.FirstSpiImpl
com.test.service.impl.SecondSpiImpl

ServiceLoader

ServiceLoader是JDK提供的專門用於實現SPI機制的類。位於java.util.ServiceLoader
ServiceLoader類的建構函式被私有化了。所以構建ServiceLoader物件只能透過ServiceLoader.load()方法。該方法有兩個過載
使用ServiceLoader時可選擇是否用自定義類載入器來載入目標類。也可預設使用應用程式類載入器載入。

jdk透過ServiceLoader類去ClassPath下的 “META-INF/services/”(此路徑約定成俗) 路徑裡查詢相應的介面實現類。ServiceLoader類核心功能就兩個點,都在ServiceLoader的內部類LazyIterator中:

  • 查詢相應介面對應實現類:hasNextService()
  • 載入相應介面實現類到虛擬機器內:nextService()
public final class ServiceLoader<S> implements Iterable<S> {


    //掃描目錄字首
    private static final String PREFIX = "META-INF/services/";

    // 被載入的類或介面
    private final Class<S> service;

    // 用於定位、載入和例項化實現方實現的類的類載入器
    private final ClassLoader loader;

    // 上下文物件
    private final AccessControlContext acc;

    // 按照例項化的順序快取已經例項化的類
    private LinkedHashMap<String, S> providers = new LinkedHashMap<>();

    // 懶查詢迭代器
    private java.util.ServiceLoader.LazyIterator lookupIterator;

    // 私有內部類,提供對所有的service的類的載入與例項化
    private class LazyIterator implements Iterator<S> {
        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        String nextName = null;

        //...
        private boolean hasNextService() {
            if (configs == null) {
                try {
                    //獲取目錄下所有的類 掃描目錄字首(META-INF/services/)+ 相應介面全限定名
                    String fullName = PREFIX + service.getName();
                    //該loader是構造ServiceLoader類時設定。可傳入自定義類載入器,如未傳入,則預設應用程式類載入器  
                    if (loader == null)
                        //在系統中查詢資源,注意查詢資源的載入器是從當前執行緒上下文中獲取。也就是預設的應用程式類載入器。所以能載入到第三方jar包下的classpath路徑。
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    //...
                }
                //....
            }
        }

        private S nextService() {
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                //反射載入類
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
            }
            try {
                //例項化
                S p = service.cast(c.newInstance());
                //放進快取
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                //..
            }
            //..
        }
    }
}

應用程式透過迭代器介面獲取物件例項,這裡首先會判斷 providers 物件中是否有例項物件:

  • 有例項,那麼就返回
  • 沒有,執行類的裝載步驟,具體類裝載實現如下:

LazyIterator#hasNextService 讀取 META-INF/services 下的配置檔案,獲得所有能被例項化的類的名稱,並完成 SPI 配置檔案的解析
LazyIterator#nextService 負責例項化 hasNextService() 讀到的實現類,並將例項化後的物件存放到 providers 集合中快取
image
image

應用案例

Java定義了一套JDBC的介面,但並未提供具體實現類,而是在不同廠商提供的資料庫實現包。

一般要根據自己使用的資料庫驅動jar包,比如我們最常用的MySQL,其mysql-jdbc-connector.jar 裡面就有:
image

小結

JDK中的SPI實現,是由ServiceLoader類根據自定義傳入類載入器或者應用程式類載入器在約定好的固定路徑下(ClassPath:META-INF/services/)去查詢和載入第三方介面實現類。

注意:要使用JDK中的SPI機制有幾個前提條件

  • 服務提供方必須實現目標介面
  • 服務提供方必須在自身ClassPath:META-INF/services/路徑下建立檔案,檔名為目標介面全限定名。檔案內容為實現目標介面的具體實現類全限定名

相關文章