可插拔組件設計機制—SPI

作者:京東物流 孔祥東

1.SPI 是什麼?

SPI 的全稱是Service Provider Interface,即提供服務接口;是一種服務發現機制,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-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-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 包,執行結果

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

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

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;
            }
        }
........

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

  • 應用程序調用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.對比

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章