天天看點

JDK源碼解析之Java SPI機制

1. spi 是什麼

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

系統設計的各個抽象,往往有很多不同的實作方案,在面向的對象的設計裡,一般推薦子產品之間基于接口程式設計,子產品之間不對實作類進行寫死。一旦代碼裡涉及具體的實作類,就違反了開閉原則,Java SPI就是為某個接口尋找服務實作的機制,Java Spi的核心思想就是解耦。

整體機制圖如下:

Java SPI 實際上是“基于接口的程式設計+政策模式+配置檔案”組合實作的動态加載機制。

總結起來就是:調用者根據實際使用需要,啟用、擴充、或者替換架構的實作政策

2. 應用場景

  • 資料庫驅動加載接口實作類的加載

    JDBC加載不同類型資料庫的驅動

  • 日志門面接口實作類加載

    SLF4J加載不同提供應商的日志實作類

  • Spring

    Servlet容器啟動初始化

    org.springframework.web.SpringServletContainerInitializer

  • Spring Boot

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

  • Dubbo

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

    例如Protocol 協定接口

3. 使用步驟

以支付服務為例:

  • 建立一個

    PayService

    添加一個

    pay

    方法
    package com.imooc.spi;
    
    import java.math.BigDecimal;
    
    public interface PayService {
    
        void pay(BigDecimal price);
    }
          
      
  1. 建立

    AlipayService

    WechatPayService

    ,實作

    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("使用微信支付");
        }
    }
          
  2. resources目錄下建立目錄META-INF/services
  3. 在META-INF/services建立com.imooc.spi.PayService檔案
  4. 先以AlipayService為例:在com.imooc.spi.PayService添加com.imooc.spi.AlipayService的檔案内容
  5. 建立測試類
    package com.imooc.spi;
    
    import com.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));
            }
        }
    }
          
  6. 運作測試類,檢視傳回結果

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;
  
    // 建立ServiceLoader時擷取的通路控制上下文
    private final AccessControlContext acc;
  
    // 按執行個體化順序緩存Provider
    private LinkedHashMap<String, S> providers = new LinkedHashMap();
  
    // 懶加載疊代器 
    private LazyIterator lookupIterator;
  
  	......
}
      

參考具體ServiceLoader具體源碼,代碼量不多,實作的流程如下:

  1. 應用程式調用ServiceLoader.load方法
    // 1. 擷取ClassLoad
    public static <S> ServiceLoader<S> load(Class<S> service) {
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      return ServiceLoader.load(service, cl);
    }
    
    // 2. 調用構造方法
    public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
      return new ServiceLoader<>(service, loader);
    }
    
    // 3. 校驗參數和ClassLoad
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
      service = Objects.requireNonNull(svc, "Service interface cannot be null");
      loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
      acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
      reload();
    }
    
    //4. 清理緩存容器,執行個體懶加載疊代器
    public void reload() {
      providers.clear();
      lookupIterator = new LazyIterator(service, loader);
    }
          
  2. 我們簡單看一下這個懶加載疊代器
    // 實作完全懶惰的提供程式查找的私有内部類
    private class LazyIterator implements Iterator<S>{
    
      // 需要加載的服務的類或接口
      Class<S> service;
      // 用于定位、加載和執行個體化提供程式的類加載器
      ClassLoader loader;
      // 枚舉類型的資源路徑
      Enumeration<URL> configs = null;
      // 疊代器
      Iterator<String> pending = null;
      // 配置檔案中下一行className
      String nextName = null;
    
      private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
      }
    
      private boolean hasNextService() {
        if (nextName != null) {
          return true;
        }
        // 加載配置PREFIX + service.getName()的檔案
        if (configs == null) {
          try {
            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;
      }
    
      // 擷取下一個Service實作
      private S nextService() {
        if (!hasNextService())
          throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
          // 加載類
          c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
          fail(service,
               "Provider " + cn + " not found");
        }
        // 超類判斷
        if (!service.isAssignableFrom(c)) {
          fail(service,
               "Provider " + cn  + " not a subtype");
        }
        try {
          // 執行個體化并進行類轉換
          S p = service.cast(c.newInstance());
          // 放入緩存容器中
          providers.put(cn, p);
          return p;
        } catch (Throwable x) {
          fail(service,
               "Provider " + cn + " could not be instantiated",
               x);
        }
        throw new Error();          // This cannot happen
      }
    
      // for循環周遊時
      public boolean hasNext() {
        if (acc == null) {
          return hasNextService();
        } else {
          PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
            public Boolean run() { return hasNextService(); }
          };
          return AccessController.doPrivileged(action, acc);
        }
      }
    
      public S next() {
        if (acc == null) {
          return nextService();
        } else {
          PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
          };
          return AccessController.doPrivileged(action, acc);
        }
      }
    
      // 禁止删除
      public void remove() {
        throw new UnsupportedOperationException();
      }
    
    }
          
  3. 将給定URL的内容作為提供程式配置檔案進行分析。
    private Iterator<String> parse(Class<?> service, URL u)
            throws ServiceConfigurationError
        {
            InputStream in = null;
            BufferedReader r = null;
            ArrayList<String> names = new ArrayList<>();
            try {
                in = u.openStream();
                r = new BufferedReader(new InputStreamReader(in, "utf-8"));
                int lc = 1;
                while ((lc = parseLine(service, u, r, lc, names)) >= 0);
            } catch (IOException x) {
                fail(service, "Error reading configuration file", x);
            } finally {
                try {
                    if (r != null) r.close();
                    if (in != null) in.close();
                } catch (IOException y) {
                    fail(service, "Error closing configuration file", y);
                }
            }
            return names.iterator();
        }
          
  4. 按行解析配置檔案,并儲存names清單中
    private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                              List<String> names)
            throws IOException, ServiceConfigurationError
        {
            String ln = r.readLine();
            if (ln == null) {
                return -1;
            }
            int ci = ln.indexOf('#');
            if (ci >= 0) ln = ln.substring(0, ci);
            ln = ln.trim();
            int n = ln.length();
            if (n != 0) {
                if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                    fail(service, u, lc, "Illegal configuration-file syntax");
                int cp = ln.codePointAt(0);
                if (!Character.isJavaIdentifierStart(cp))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
                for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                    cp = ln.codePointAt(i);
                    if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                        fail(service, u, lc, "Illegal provider-class name: " + ln);
                }
                // 判斷provider容器中是否包含 不包含則講classname加入 names清單中
                if (!providers.containsKey(ln) && !names.contains(ln))
                    names.add(ln);
            }
            return lc + 1;
        }
          
     

5. 總結

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

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