JDK原始碼解析之Java SPI機制

Java勸退師發表於2019-04-01

1. spi 是什麼

SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實現或者擴充套件的API,它可以用來啟用框架擴充套件和替換元件。

系統設計的各個抽象,往往有很多不同的實現方案,在面向的物件的設計裡,一般推薦模組之間基於介面程式設計,模組之間不對實現類進行硬編碼。一旦程式碼裡涉及具體的實現類,就違反了開閉原則,Java SPI就是為某個介面尋找服務實現的機制,Java Spi的核心思想就是解耦

整體機制圖如下:

JDK原始碼解析之Java SPI機制

Java SPI 實際上是“基於介面的程式設計+策略模式+配置檔案”組合實現的動態載入機制。

總結起來就是:呼叫者根據實際使用需要,啟用、擴充套件、或者替換框架的實現策略

2. 應用場景

  • 資料庫驅動載入介面實現類的載入

    JDBC載入不同型別資料庫的驅動

  • 日誌門面介面實現類載入

    SLF4J載入不同提供應商的日誌實現類

  • Spring Spring Boot

    自動裝配過程中,載入META-INF/spring.factories檔案,解析properties檔案

  • Dubbo

    Dubbo大量使用了SPI技術,裡面有很多個元件,每個元件在框架中都是以介面的形成抽象出來

    例如Protocol 協議介面

3. 使用步驟

以支付服務為例:

  1. 建立一個PayService新增一個pay方法

    package com.imooc.spi;
    
    import java.math.BigDecimal;
    
    public interface PayService {
    
        void pay(BigDecimal price);
    }
    複製程式碼
  2. 建立AlipayServiceWechatPayService,實現PayService

    ⚠️SPI的實現類必須攜帶一個不帶引數的構造方法;

    package com.imooc.spi;
    
    import java.math.BigDecimal;
    
    public class AlipayService implements PayService{
    
        public void pay(BigDecimal price) {
            System.out.println("使用支付寶支付");
        }
    }
    複製程式碼
    package com.imooc.spi;
    
    import java.math.BigDecimal;
    
    public class WechatPayService implements PayService{
    
        public void pay(BigDecimal price) {
            System.out.println("使用微信支付");
        }
    }
    複製程式碼
  3. resources目錄下建立目錄META-INF/services

  4. 在META-INF/services建立com.imooc.spi.PayService檔案

  5. 先以AlipayService為例:在com.imooc.spi.PayService新增com.imooc.spi.AlipayService的檔案內容

  6. 建立測試類

    package com.imooc.spi;
    
    import com.sun.tools.javac.util.ServiceLoader;
    
    import java.math.BigDecimal;
    
    public class PayTests {
    
        public static void main(String[] args) {
            ServiceLoader<PayService> payServices = ServiceLoader.load(PayService.class);
            for (PayService payService : payServices) {
                payService.pay(new BigDecimal(1));
            }
        }
    }
    複製程式碼
  7. 執行測試類,檢視返回結果

    使用支付寶支付
    複製程式碼

4. 原理分析

首先,我們先開啟ServiceLoader<S> 這個類

public final class ServiceLoader<S> implements Iterable<S> {
  
  	// SPI檔案路徑的字首
    private static final String PREFIX = "META-INF/services/";
  
    // 需要載入類的介面
    private Class<S> service;
  
    // 類載入器
    private ClassLoader loader;
  
    // 快取providers,儲存著service實現
    private LinkedHashMap<String, S> providers = new LinkedHashMap();
  
    // 懶載入的查詢迭代器 
    private ServiceLoader<S>.LazyIterator lookupIterator;
  
  	......
}
複製程式碼

參考具體ServiceLoader具體原始碼,程式碼量不多,實現的流程如下:

  1. 應用程式呼叫ServiceLoader.load方法

    // 1. 獲取ClassLoad
    public static <S> ServiceLoader<S> load(Class<S> var0) {
      ClassLoader var1 = Thread.currentThread().getContextClassLoader();
      return load(var0, var1);
    }
    
    // 2. 呼叫構造方法
    public static <S> ServiceLoader<S> load(Class<S> var0, ClassLoader var1) {
      return new ServiceLoader(var0, var1);
    }
    
    // 3. 校驗引數和ClassLoad
    private ServiceLoader(Class<S> var1, ClassLoader var2) {
      this.service = (Class)Objects.requireNonNull(var1, "Service interface cannot be null");
      this.loader = var2 == null ? ClassLoader.getSystemClassLoader() : var2;
      this.reload();
    }
    
    //4. 清理快取容器,例項懶載入迭代器
    public void reload() {
      this.providers.clear();
      this.lookupIterator = new ServiceLoader.LazyIterator(this.service, this.loader, null);
    }
    複製程式碼
  2. 我們簡單看一下這個懶載入迭代器

    private class LazyIterator implements Iterator<S> {
      Class<S> service;
      ClassLoader loader;
      Enumeration<URL> configs;
      Iterator<String> pending;
      String nextName;
    
      private LazyIterator(Class<S> var1, ClassLoader var2) {
        this.configs = null;
        this.pending = null;
        this.nextName = null;
        this.service = var2;
        this.loader = var3;
      }
    
      // 迭代執行並獲取解析出來的com.imooc.spi.AlipayService
      public boolean hasNext() {
        if (this.nextName != null) {
          return true;
        } else {
          if (this.configs == null) {
            try {
              String var1 = "META-INF/services/" + this.service.getName();
              if (this.loader == null) {
                this.configs = ClassLoader.getSystemResources(var1);
              } else {
                this.configs = this.loader.getResources(var1);
              }
            } catch (IOException var2) {
              ServiceLoader.fail(this.service, "Error locating configuration files", var2);
            }
          }
    
          while(this.pending == null || !this.pending.hasNext()) {
            if (!this.configs.hasMoreElements()) {
              return false;
            }
    
            this.pending = ServiceLoader.this.parse(this.service, (URL)this.configs.nextElement());
          }
    
          this.nextName = (String)this.pending.next();
          return true;
        }
      }
    
      public S next() {
        if (!this.hasNext()) {
          throw new NoSuchElementException();
        } else {
          String var1 = this.nextName;
          this.nextName = null;
          Class var2 = null;
    
          try {
            // 通過反射方法Class.forName()載入類物件
            var2 = Class.forName(var1, false, this.loader);
          } catch (ClassNotFoundException var5) {
            ServiceLoader.fail(this.service, "Provider " + var1 + " not found");
          }
    
          if (!this.service.isAssignableFrom(var2)) {
            ServiceLoader.fail(this.service, "Provider " + var1 + " not a subtype");
          }
    
          try {
            // 呼叫instance()方法將類例項化
            Object var3 = this.service.cast(var2.newInstance());
            // 儲存容器
            ServiceLoader.this.providers.put(var1, var3);
            // 並返回例項
            return var3;
          } catch (Throwable var4) {
            ServiceLoader.fail(this.service, "Provider " + var1 + " could not be instantiated: " + var4, var4);
            throw new Error();
          }
        }
      }
    
      // 禁止刪除
      public void remove() {
        throw new UnsupportedOperationException();
      }
    }
    複製程式碼

5. 總結

優點:使用Java SPI機制的優勢是實現解耦,使得第三方服務模組的裝配控制的邏輯與呼叫者的業務程式碼分離,而不是耦合在一起。應用程式可以根據實際業務情況啟用框架擴充套件或替換框架元件。

缺點:執行緒不安全,雖然ServiceLoader也算是使用的延遲載入,但是基本只能通過遍歷全部獲取,也就是介面的實現類全部載入並例項化一遍。如果你並不想用某些實現類,它也被載入並例項化了,這就造成了浪費。獲取某個實現類的方式不夠靈活,只能通過Iterator形式獲取,不能根據某個引數來獲取對應的實現類。

相關文章