天天看點

可插拔元件設計機制—SPI

作者:京東雲開發者

#科技之巅#

作者:京東物流 孔祥東

1.SPI 是什麼?

SPI 的全稱是Service Provider Interface,即提供服務接口;是一種服務發現機制,SPI 的本質是将接口實作類的全限定名配置在檔案中,并由服務加載器讀取配置檔案,加載實作類。這樣可以在運作時,動态為接口替換實作類。正是以特性,我們可以很容易的通過 SPI 機制為我們的程式提供拓展功能。

如下圖:

可插拔元件設計機制—SPI

系統設計的各個抽象,往往有很多不同的實作方案,在面對象設計裡,一般推薦子產品之間基于接口程式設計,子產品之間不對實作寫死,一旦代碼涉及具體的實作類,就違反了可插拔的原則。Java SPI 就是提供這樣的一個機制,為某一個接口尋找服務的實作,有點類似IOC 的思想,把裝配的控制權移到程式之外,在子產品化涉及裡面這個各尤為重要。與其說SPI 是java 提供的一種服務發現機制,倒不如說是一種解耦思想。

2.使用場景?

  • 資料庫驅動加載接口實作類的加載;如:JDBC 加載Mysql,Oracle...
  • 日志門面接口實作類加載,如:SLF4J 對log4j、logback 的支援
  • Spring中大量使用了SPI,特别是spring-boot 中自動化配置的實作
  • Dubbo 也是大量使用SPI 的方式實作架構的擴充,它是對原生的SPI 做了封裝,允許使用者擴充實作Filter 接口。

3.使用介紹

要使用 Java SPI,需要遵循以下約定:

  • 當服務提供者提供了接口的一種具體實作後,需要在JAR 包的META-INF/services 目錄下建立一個以“接口全限制定名”為命名的檔案,内容為實作類的全限定名;
  • 接口實作類所在的JAR放在主程式的classpath 下,也就是引入依賴。
  • 主程式通過java.util.ServiceLoder 動态加載實作子產品,它會通過掃描META-INF/services 目錄下的檔案找到實作類的全限定名,把類加載值JVM,并執行個體化它;
  • SPI 的實作類必須攜帶一個不帶參數的構造方法。

示例:

可插拔元件設計機制—SPI

spi-interface 子產品定義

定義一組接口:public interface MyDriver            

spi-jd-driver

spi-ali-driver

實作為:public class JdDriver implements MyDriver
  public class AliDriver implements MyDriver            

在 src/main/resources/ 下建立 /META-INF/services 目錄, 新增一個以接口命名的檔案 (org.MyDriver 檔案)

内容是要應用的實作類分别 com.jd.JdDriver和com.ali.AliDriver

可插拔元件設計機制—SPI

spi-core

一般都是平台提供的核心包,包含加載使用實作類的政策等等,我們這邊就簡單實作一下邏輯:a.沒有找到具體實作抛出異常 b.如果發現多個實作,分别列印

public void invoker(){
    ServiceLoader<MyDriver>  serviceLoader = ServiceLoader.load(MyDriver.class);
    Iterator<MyDriver> drivers = serviceLoader.iterator();
    boolean isNotFound = true;
    while (drivers.hasNext()){
        isNotFound = false;
        drivers.next().load();
    }
    if(isNotFound){
        throw new RuntimeException("一個驅動實作類都不存在");
    }
}           

spi-test

public class App 
{
    public static void main( String[] args )
    {
        DriverFactory factory = new DriverFactory();
        factory.invoker();
    }
}           

1.引入spi-core 包,執行結果

可插拔元件設計機制—SPI

2.引入spi-core,spi-jd-driver 包

可插拔元件設計機制—SPI

3.引入spi-core,spi-jd-driver,spi-ali-driver

可插拔元件設計機制—SPI

4.原了解析

看看我們剛剛是怎麼拿到具體的實作類的?

就兩行代碼:

ServiceLoader<MyDriver>  serviceLoader = ServiceLoader.load(MyDriver.class);
Iterator<MyDriver> drivers = serviceLoader.iterator();           

是以,首先我們看ServiceLoader 類:

public final class ServiceLoader<S> implements Iterable<S>{
//配置檔案的路徑
 private static final String PREFIX = "META-INF/services/";
    // 代表被加載的類或者接口
    private final Class<S> service;
    // 用于定位,加載和執行個體化providers的類加載器
    private final ClassLoader loader;
    // 建立ServiceLoader時采用的通路控制上下文
    private final AccessControlContext acc;
    // 緩存providers,按執行個體化的順序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 懶查找疊代器,真正加載服務的類
    private LazyIterator lookupIterator;
  
 //服務提供者查找的疊代器
    private class LazyIterator
        implements Iterator<S>
    {
 .....
private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
//全限定名:com.xxxx.xxx
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }


        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
//通過反射擷取
                c = Class.forName(cn, false, loader);
            }
            if (!service.isAssignableFrom(c)) {
                fail(service, "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            }
        }
........           

大概的流程就是下面這張圖:

可插拔元件設計機制—SPI
  • 應用程式調用ServiceLoader.load 方法
  • 應用程式通過疊代器擷取對象執行個體,會先判斷providers對象中是否已經有緩存的示例對象,如果存在直接傳回
  • 如果沒有存在,執行類轉載讀取META-INF/services 下的配置檔案,擷取所有能被執行個體化的類的名稱,可以跨越JAR 擷取配置檔案通過反射方法Class.forName()加載對象并用Instance() 方法示例化類将執行個體化類緩存至providers對象中,同步傳回。

5.總結

優點:解耦

SPI 的使用,使得第三方服務子產品的裝配控制邏輯與調用者的業務代碼分離,不會耦合在一起,應用程式可以根據實際業務情況來啟用架構擴充和替換架構元件。

SPI 的使用,使得無須通過下面幾種方式擷取實作類

  • 代碼寫死import 導入
  • 指定類全限定名反射擷取,例如JDBC4.0 之前;Class.forName("com.mysql.jdbc.Driver")

缺點:

雖然ServiceLoader也算是使用的延遲加載,但是基本隻能通過周遊全部擷取,也就是接口的實作類全部加載并執行個體化一遍。如果你并不想用某些實作類,它也被加載并執行個體化了,這就造成了浪費。擷取某個實作類的方式不夠靈活,隻能通過Iterator形式擷取,不能根據某個參數來擷取對應的實作類。

6.對比

JDK SPI DUBBO SPI Spring SPI
檔案方式 每個擴充點單獨一個檔案 每個擴充點單獨一個檔案 所有的擴充點在一個檔案
擷取某個固定的實作 不支援,隻能按順序擷取所有實作 有“别名”的概念,可以通過名稱擷取擴充點的某個固定實作,配合Dubbo SPI的注解很友善 不支援,隻能按順序擷取所有實作。但由于Spring Boot ClassLoader會優先加載使用者代碼中的檔案,是以可以保證使用者自定義的spring.factoires檔案在第一個,通過擷取第一個factory的方式就可以固定擷取自定義的擴充
其他 支援Dubbo内部的依賴注入,通過目錄來區分Dubbo 内置SPI和外部SPI,優先加載内部,保證内部的優先級最高
文檔完整度 文章 & 三方資料足夠豐富 文檔 & 三方資料足夠豐富 文檔不夠豐富,但由于功能少,使用非常簡單
IDE支援 IDEA 完美支援,有文法提示

繼續閱讀