天天看點

源碼級深度了解 Java SPI

作者:vivo 網際網路伺服器團隊- Zhang Peng

SPI 是一種用于動态加載服務的機制。它的核心思想就是解耦,屬于典型的微核心架構模式。SPI 在 Java 世界應用非常廣泛,如:Dubbo、Spring Boot 等架構。本文從源碼入手分析,深入探讨 Java SPI 的特性、原理,以及在一些比較經典領域的應用。

一、SPI 簡介

SPI 全稱 Service Provider Interface,是 Java 提供的,旨在由第三方實作或擴充的 API,它是一種用于動态加載服務的機制。Java 中 SPI 機制主要思想是将裝配的控制權移到程式之外,在子產品化設計中這個機制尤其重要,其核心思想就是 解耦。

Java SPI 有四個要素:

  • SPI 接口:為服務提供者實作類約定的的接口或抽象類。
  • SPI 實作類:實際提供服務的實作類。
  • SPI 配置:Java SPI 機制約定的配置檔案,提供查找服務實作類的邏輯。配置檔案必須置于 META-INF/services 目錄中,并且,檔案名應與服務提供者接口的完全限定名保持一緻。檔案中的每一行都有一個實作服務類的詳細資訊,同樣是服務提供者類的完全限定名稱。
  • ServiceLoader:Java SPI 的核心類,用于加載 SPI 實作類。ServiceLoader 中有各種實用方法來擷取特定實作、疊代它們或重新加載服務。

二、SPI 示例

正所謂,實踐出真知,我們不妨通過一個具體的示例來看一下,如何使用 Java SPI。

2.1 SPI 接口

首先,需要定義一個 SPI 接口,和普通接口并沒有什麼差别。

package io.github.dunwu.javacore.spi;

public interface DataStorage {
    String search(String key);
}      

2.2 SPI 實作類

假設,我們需要在程式中使用兩種不同的資料存儲——MySQL 和 Redis。是以,我們需要兩個不同的實作類去分别完成相應工作。

MySQL查詢 MOCK 類

package io.github.dunwu.javacore.spi;

public class MysqlStorage implements DataStorage {
    @Override
    public String search(String key) {
        return "【Mysql】搜尋" + key + ",結果:No";
    }
}      

Redis 查詢 MOCK 類

package io.github.dunwu.javacore.spi;

public class RedisStorage implements DataStorage {
    @Override
    public String search(String key) {
        return "【Redis】搜尋" + key + ",結果:Yes";
    }
}      

service 傳入的是期望加載的 SPI 接口類型 到目前為止,定義接口,并實作接口和普通的 Java 接口實作沒有任何不同。

2.3 SPI 配置

如果想通過 Java SPI 機制來發現服務,就需要在 SPI 配置中約定好發現服務的邏輯。配置檔案必須置于 META-INF/services 目錄中,并且,檔案名應與服務提供者接口的完全限定名保持一緻。檔案中的每一行都有一個實作服務類的詳細資訊,同樣是服務提供者類的完全限定名稱。以本示例代碼為例,其檔案名應該為io.github.dunwu.javacore.spi.DataStorage,

檔案中的内容如下:

io.github.dunwu.javacore.spi.MysqlStorage
io.github.dunwu.javacore.spi.RedisStorage      

2.4 ServiceLoader

完成了上面的步驟,就可以通過 ServiceLoader 來加載服務。示例如下:

import java.util.ServiceLoader;

public class SpiDemo {

    public static void main(String[] args) {
        ServiceLoader<DataStorage> serviceLoader = ServiceLoader.load(DataStorage.class);
        System.out.println("============ Java SPI 測試============");
        serviceLoader.forEach(loader -> System.out.println(loader.search("Yes Or No")));
    }

}      

輸出:

============ Java SPI 測試============
【Mysql】搜尋Yes Or No,結果:No
【Redis】搜尋Yes Or No,結果:Yes      

三、SPI 原理

上文中,我們已經了解 Java SPI 的要素以及使用 Java SPI 的方法。你有沒有想過,Java SPI 和普通 Java 接口有何不同,Java SPI 是如何工作的。實際上,Java SPI 機制依賴于 ServiceLoader 類去解析、加載服務。是以,掌握了 ServiceLoader 的工作流程,就掌握了 SPI 的原理。ServiceLoader 的代碼本身很精練,接下來,讓我們通過走讀源碼的方式,逐一了解 ServiceLoader 的工作流程。

3.1 ServiceLoader 的成員變量

先看一下 ServiceLoader 類的成員變量,大緻有個印象,後面的源碼中都會使用到。

public final class ServiceLoader<S> implements Iterable<S> {

    // SPI 配置檔案目錄
    private static final String PREFIX = "META-INF/services/";

    // 将要被加載的 SPI 服務
    private final Class<S> service;

    // 用于加載 SPI 服務的類加載器
    private final ClassLoader loader;

    // ServiceLoader 建立時的通路控制上下文
    private final AccessControlContext acc;

    // SPI 服務緩存,按執行個體化的順序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 懶查詢疊代器
    private LazyIterator lookupIterator;

    // ...
}      

3.2 ServiceLoader 的工作流程

(1)ServiceLoader.load 靜态方法

應用程式加載 Java SPI 服務,都是先調用 ServiceLoader.load 靜态方法。

ServiceLoader.load 靜态方法的作用是:

① 指定類加載 ClassLoader 和通路控制上下文;

② 然後,重新加載 SPI 服務

  • 清空緩存中所有已執行個體化的 SPI 服務
  • 根據 ClassLoader 和 SPI 類型,建立懶加載疊代器

這裡,摘錄 ServiceLoader.load 相關源碼,如下:

// service 傳入的是期望加載的 SPI 接口類型
// loader 是用于加載 SPI 服務的類加載器
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
  return new ServiceLoader<>(service, loader);
}

public void reload() {
    // 清空緩存中所有已執行個體化的 SPI 服務
  providers.clear();
    // 根據 ClassLoader 和 SPI 類型,建立懶加載疊代器
  lookupIterator = new LazyIterator(service, loader);
}

// 私有構造方法
// 重新加載 SPI 服務
private ServiceLoader(Class<S> svc, ClassLoader cl) {
  service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // 指定類加載 ClassLoader 和通路控制上下文
  loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
  acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    // 然後,重新加載 SPI 服務
  reload();
}      

(2)應用程式通過 ServiceLoader 的 iterator 方法周遊 SPI 執行個體

ServiceLoader 的類定義,明确了 ServiceLoader 類實作了 Iterable<T> 接口,是以,它是可以疊代周遊的。實際上,ServiceLoader 類維護了一個緩存 providers( LinkedHashMap 對象),緩存 providers 中儲存了已經被成功加載的 SPI 執行個體,這個 Map 的 key 是 SPI 接口實作類的全限定名,value 是該實作類的一個執行個體對象。

當應用程式調用 ServiceLoader 的 iterator 方法時,ServiceLoader 會先判斷緩存 providers 中是否有資料:如果有,則直接傳回緩存 providers 的疊代器;如果沒有,則傳回懶加載疊代器的疊代器。

public Iterator<S> iterator() {
  return new Iterator<S>() {

        // 緩存 SPI providers
    Iterator<Map.Entry<String,S>> knownProviders
      = providers.entrySet().iterator();

        // lookupIterator 是 LazyIterator 執行個體,用于懶加載 SPI 執行個體
    public boolean hasNext() {
      if (knownProviders.hasNext())
        return true;
      return lookupIterator.hasNext();
    }

    public S next() {
      if (knownProviders.hasNext())
        return knownProviders.next().getValue();
      return lookupIterator.next();
    }

    public void remove() {
      throw new UnsupportedOperationException();
    }

  };
}      

(3)懶加載疊代器的工作流程

上面的源碼中提到了,lookupIterator 是 LazyIterator 執行個體,而 LazyIterator 用于懶加載 SPI 執行個體。那麼, LazyIterator 是如何工作的呢?

這裡,摘取 LazyIterator 關鍵代碼

hasNextService 方法:
  • 拼接 META-INF/services/ + SPI 接口全限定名
  • 通過類加載器,嘗試加載資源檔案
  • 解析資源檔案中的内容,擷取 SPI 接口的實作類的全限定名 nextName
nextService 方法:
  • hasNextService() 方法解析出了 SPI 實作類的的全限定名 nextName,通過反射,擷取 SPI 實作類的類定義 Class。
  • 然後,嘗試通過 Class 的 newInstance 方法執行個體化一個 SPI 服務對象。如果成功,則将這個對象加入到緩存 providers 中并傳回該對象。
private boolean hasNextService() {
  if (nextName != null) {
    return true;
  }
  if (configs == null) {
    try {
            // 1.拼接 META-INF/services/ + SPI 接口全限定名
            // 2.通過類加載器,嘗試加載資源檔案
            // 3.解析資源檔案中的内容
      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;
}

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 s");
  }
  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
}      

3.3 SPI 和類加載器

通過上面兩個章節中,走讀 ServiceLoader 代碼,我們已經大緻了解 Java SPI 的工作原理,即通過 ClassLoader 加載 SPI 配置檔案,解析 SPI 服務,然後通過反射,執行個體化 SPI 服務執行個體。我們不妨思考一下,為什麼加載 SPI 服務時,需要指定類加載器 ClassLoader 呢?

學習過 JVM 的讀者,想必都了解過類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的 BootstrapClassLoader 外,其餘的類加載器都應有自己的父類加載器。這裡類加載器之間的父子關系一般通過組合(Composition)關系來實作,而不是通過繼承(Inheritance)的關系實作。

雙親委派機制約定了:一個類加載器首先将類加載請求傳送到父類加載器,隻有當父類加載器無法完成類加載請求時才嘗試加載。

雙親委派的好處:使得 Java 類伴随着它的類加載器,天然具備一種帶有優先級的層次關系,進而使得類加載得到統一,不會出現重複加載的問題:

  1. 系統類防止記憶體中出現多份同樣的位元組碼
  2. 保證 Java 程式安全穩定運作

例如:java.lang.Object 存放在 rt.jar 中,如果編寫另外一個 java.lang.Object 的類并放到 classpath 中,程式可以編譯通過。因為雙親委派模型的存在,是以在 rt.jar 中的 Object 比在 classpath 中的 Object 優先級更高,因為 rt.jar 中的 Object 使用的是啟動類加載器,而 classpath 中的 Object 使用的是應用程式類加載器。正因為 rt.jar 中的 Object 優先級更高,因為程式中所有的 Object 都是這個 Object。

雙親委派的限制:子類加載器可以使用父類加載器已經加載的類,而父類加載器無法使用子類加載器已經加載的。——這就導緻了雙親委派模型并不能解決所有的類加載器問題。Java SPI 就面臨着這樣的問題:

  • SPI 的接口是 Java 核心庫的一部分,是由 BootstrapClassLoader 加載的;
  • 而 SPI 實作的 Java 類一般是由 AppClassLoader 來加載的。BootstrapClassLoader 是無法找到 SPI 的實作類的,因為它隻加載 Java 的核心庫。它也不能代理給 AppClassLoader,因為它是最頂層的類加載器。這也解釋了本節開始的問題——為什麼加載 SPI 服務時,需要指定類加載器 ClassLoader 呢?因為如果不指定 ClassLoader,則無法擷取 SPI 服務。

如果不做任何的設定,Java 應用的線程的上下文類加載器預設就是 AppClassLoader。在核心類庫使用 SPI 接口時,傳遞的類加載器使用線程上下文類加載器,就可以成功的加載到 SPI 實作的類。線程上下文類加載器在很多 SPI 的實作中都會用到。

通常可以通過Thread.currentThread().getClassLoader()和 Thread.currentThread().getContextClassLoader() 擷取線程上下文類加載器。

3.4 Java SPI 的不足

Java SPI 存在一些不足:

  • 不能按需加載,需要周遊所有的實作,并執行個體化,然後在循環中才能找到我們需要的實作。如果不想用某些實作類,或者某些類執行個體化很耗時,它也被載入并執行個體化了,這就造成了浪費。
  • 擷取某個實作類的方式不夠靈活,隻能通過 Iterator 形式擷取,不能根據某個參數來擷取對應的實作類。
  • 多個并發多線程使用 ServiceLoader 類的執行個體是不安全的。

四、SPI 應用場景

SPI 在 Java 開發中應用十分廣泛。首先,在 Java 的 java.util.spi package 中就約定了很多 SPI 接口。下面,列舉一些 SPI 接口:

  • ​​TimeZoneNameProvider​​: 為 TimeZone 類提供本地化的時區名稱。
  • ​​DateFormatProvider​​: 為指定的語言環境提供日期和時間格式。
  • ​​NumberFormatProvider​​: 為 NumberFormat 類提供貨币、整數和百分比值。
  • ​​Driver​​: 從 4.0 版開始,JDBC API 支援 SPI 模式。舊版本使用 Class.forName() 方法加載驅動程式。
  • ​​PersistenceProvider​​: 提供 JPA API 的實作。
  • 等等

除此以外,SPI 還有很多應用,下面列舉幾個經典案例。

4.1 SPI 應用案例之 JDBC DriverManager

作為 Java 工程師,尤其是 CRUD 工程師,相必都非常熟悉 JDBC。衆所周知,關系型資料庫有很多種,如:MySQL、Oracle、PostgreSQL 等等。JDBC 如何識别各種資料庫的驅動呢?

4.1.1 建立資料庫連接配接

我們先回顧一下,JDBC 如何建立資料庫連接配接的呢?

在 JDBC4.0 之前,連接配接資料庫的時候,通常會用 Class.forName(XXX) 方法來加載資料庫相應的驅動,然後再擷取資料庫連接配接,繼而進行 CRUD 等操作。

Class.forName("com.mysql.jdbc.Driver")      

而 JDBC4.0 之後,不再需要用Class.forName(XXX) 方法來加載資料庫驅動,直接擷取連接配接就可以了。顯然,這種方式很友善,但是如何做到的呢?

(1)JDBC 接口:首先,Java 中内置了接口 java.sql.Driver。

(2)JDBC 接口實作:各個資料庫的驅動自行實作 java.sql.Driver 接口,用于管理資料庫連接配接。

  • MySQL:在 MySQL的 Java 驅動包 mysql-connector-java-XXX.jar 中,可以找到 META-INF/services 目錄,該目錄下會有一個名字為java.sql.Driver 的檔案,檔案内容是com.mysql.cj.jdbc.Driver。

com.mysql.cj.jdbc.Driver 正是 MySQL 版的 java.sql.Driver 實作。如下圖所示:

源碼級深度了解 Java SPI
  • PostgreSQL 實作:在 PostgreSQL 的 Java 驅動包 postgresql-42.0.0.jar 中,也可以找到同樣的配置檔案,檔案内容是 org.postgresql.Driver,org.postgresql.Driver 正是 PostgreSQL 版的 java.sql.Driver 實作。

(3)建立資料庫連接配接

以 MySQL 為例,建立資料庫連接配接代碼如下:

final String DB_URL = String.format("jdbc:mysql://%s:%s/%s", DB_HOST, DB_PORT, DB_SCHEMA);
connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);      
4.1.2 DriverManager

從前文,我們已經知道 DriverManager 是建立資料庫連接配接的關鍵。它究竟是如何工作的呢?

可以看到是加載執行個體化驅動的,接着看 loadInitialDrivers 方法:

private static void loadInitialDrivers() {
  String drivers;
  try {
    drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
      public String run() {
        return System.getProperty("jdbc.drivers");
      }
    });
  } catch (Exception ex) {
    drivers = null;
  }
  // 通過 classloader 擷取所有實作 java.sql.Driver 的驅動類
  AccessController.doPrivileged(new PrivilegedAction<Void>() {
    public Void run() {
            // 利用 SPI,記載所有 Driver 服務
      ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            // 擷取疊代器
      Iterator<Driver> driversIterator = loadedDrivers.iterator();
      try{
                // 周遊疊代器
        while(driversIterator.hasNext()) {
          driversIterator.next();
        }
      } catch(Throwable t) {
      // Do nothing
      }
      return null;
    }
  });

    // 列印資料庫驅動資訊
  println("DriverManager.initialize: jdbc.drivers = " + drivers);

  if (drivers == null || drivers.equals("")) {
    return;
  }
  String[] driversList = drivers.split(":");
  println("number of Drivers:" + driversList.length);
  for (String aDriver : driversList) {
    try {
      println("DriverManager.Initialize: loading " + aDriver);
            // 嘗試執行個體化驅動
      Class.forName(aDriver, true,
          ClassLoader.getSystemClassLoader());
    } catch (Exception ex) {
      println("DriverManager.Initialize: load failed: " + ex);
    }
  }
}      

上面的代碼主要步驟是:

  1. 從系統變量中擷取驅動的實作類。
  2. 利用 SPI 來擷取所有驅動的實作類。
  3. 周遊所有驅動,嘗試執行個體化各個實作類。
  4. 根據第 1 步擷取到的驅動清單來執行個體化具體的實作類。

需要關注的是下面這行代碼:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);      

這裡實際擷取的是java.util.ServiceLoader.LazyIterator 疊代器。調用其 hasNext 方法時,會搜尋 classpath 下以及 jar 包中的 META-INF/services 目錄,查找 java.sql.Driver 檔案,并找到檔案中的驅動實作類的全限定名。調用其 next 方法時,會根據驅動類的全限定名去嘗試執行個體化一個驅動類的對象。

4.2 SPI 應用案例之 Common-Loggin

common-logging(也稱 Jakarta Commons Logging,縮寫 JCL)是常用的日志門面工具包。common-logging 的核心類是入口是 LogFactory,LogFatory 是一個抽象類,它負責加載具體的日志實作。

其入口方法是 LogFactory.getLog 方法,源碼如下:

public static Log getLog(Class clazz) throws LogConfigurationException {
  return getFactory().getInstance(clazz);
}

public static Log getLog(String name) throws LogConfigurationException {
  return getFactory().getInstance(name);
}      

從以上源碼可知,getLog 采用了工廠設計模式,是先調用 getFactory 方法擷取具體日志庫的工廠類,然後根據類名稱或類型建立日志執行個體。

LogFatory.getFactory 方法負責選出比對的日志工廠,其源碼如下:

public static LogFactory getFactory() throws LogConfigurationException {
  // 省略...

  // 加載 commons-logging.properties 配置檔案
  Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);

  // 省略...

    // 決定建立哪個 LogFactory 執行個體
  // (1)嘗試讀取全局屬性 org.apache.commons.logging.LogFactory
  if (isDiagnosticsEnabled()) {
    logDiagnostic("[LOOKUP] Looking for system property [" + FACTORY_PROPERTY +
            "] to define the LogFactory subclass to use...");
  }

  try {
        // 如果指定了 org.apache.commons.logging.LogFactory 屬性,嘗試執行個體化具體實作類
    String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);
    if (factoryClass != null) {
      if (isDiagnosticsEnabled()) {
        logDiagnostic("[LOOKUP] Creating an instance of LogFactory class '" + factoryClass +
                "' as specified by system property " + FACTORY_PROPERTY);
      }
      factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
    } else {
      if (isDiagnosticsEnabled()) {
        logDiagnostic("[LOOKUP] No system property [" + FACTORY_PROPERTY + "] defined.");
      }
    }
  } catch (SecurityException e) {
      // 異常處理
  } catch (RuntimeException e) {
      // 異常處理
  }

    // (2)利用 Java SPI 機制,嘗試在 classpatch 的 META-INF/services 目錄下尋找 org.apache.commons.logging.LogFactory 實作類
  if (factory == null) {
    if (isDiagnosticsEnabled()) {
      logDiagnostic("[LOOKUP] Looking for a resource file of name [" + SERVICE_ID +
              "] to define the LogFactory subclass to use...");
    }
    try {
      final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);

      if( is != null ) {
        // This code is needed by EBCDIC and other strange systems.
        // It's a fix for bugs reported in xerces
        BufferedReader rd;
        try {
          rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
        } catch (java.io.UnsupportedEncodingException e) {
          rd = new BufferedReader(new InputStreamReader(is));
        }

        String factoryClassName = rd.readLine();
        rd.close();

        if (factoryClassName != null && ! "".equals(factoryClassName)) {
          if (isDiagnosticsEnabled()) {
            logDiagnostic("[LOOKUP]  Creating an instance of LogFactory class " +
                    factoryClassName +
                    " as specified by file '" + SERVICE_ID +
                    "' which was present in the path of the context classloader.");
          }
          factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader );
        }
      } else {
        // is == null
        if (isDiagnosticsEnabled()) {
          logDiagnostic("[LOOKUP] No resource file with name '" + SERVICE_ID + "' found.");
        }
      }
    } catch (Exception ex) {
      // note: if the specified LogFactory class wasn't compatible with LogFactory
      // for some reason, a ClassCastException will be caught here, and attempts will
      // continue to find a compatible class.
      if (isDiagnosticsEnabled()) {
        logDiagnostic(
          "[LOOKUP] A security exception occurred while trying to create an" +
          " instance of the custom factory class" +
          ": [" + trim(ex.getMessage()) +
          "]. Trying alternative implementations...");
      }
      // ignore
    }
  }

  // (3)嘗試從 classpath 目錄下的 commons-logging.properties 檔案中查找 org.apache.commons.logging.LogFactory 屬性

  if (factory == null) {
    if (props != null) {
      if (isDiagnosticsEnabled()) {
        logDiagnostic(
          "[LOOKUP] Looking in properties file for entry with key '" + FACTORY_PROPERTY +
          "' to define the LogFactory subclass to use...");
      }
      String factoryClass = props.getProperty(FACTORY_PROPERTY);
      if (factoryClass != null) {
        if (isDiagnosticsEnabled()) {
          logDiagnostic(
            "[LOOKUP] Properties file specifies LogFactory subclass '" + factoryClass + "'");
        }
        factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);

        // TODO: think about whether we need to handle exceptions from newFactory
      } else {
        if (isDiagnosticsEnabled()) {
          logDiagnostic("[LOOKUP] Properties file has no entry specifying LogFactory subclass.");
        }
      }
    } else {
      if (isDiagnosticsEnabled()) {
        logDiagnostic("[LOOKUP] No properties file available to determine" + " LogFactory subclass from..");
      }
    }
  }

  // (4)以上情況都不滿足,執行個體化預設實作類 org.apache.commons.logging.impl.LogFactoryImpl

  if (factory == null) {
    if (isDiagnosticsEnabled()) {
      logDiagnostic(
        "[LOOKUP] Loading the default LogFactory implementation '" + FACTORY_DEFAULT +
        "' via the same classloader that loaded this LogFactory" +
        " class (ie not looking in the context classloader).");
    }

    factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);
  }

  if (factory != null) {
    /**
     * Always cache using context class loader.
     */
    cacheFactory(contextClassLoader, factory);

    if (props != null) {
      Enumeration names = props.propertyNames();
      while (names.hasMoreElements()) {
        String name = (String) names.nextElement();
        String value = props.getProperty(name);
        factory.setAttribute(name, value);
      }
    }
  }

  return factory;
}      

從 getFactory 方法的源碼可以看出,其核心邏輯分為 4 步:

  • 首先,嘗試查找全局屬性org.apache.commons.logging.LogFactory,如果指定了具體類,嘗試建立執行個體。
  • 利用 Java SPI 機制,嘗試在 classpatch 的 META-INF/services 目錄下尋找org.apache.commons.logging.LogFactory 的實作類。
  • 嘗試從 classpath 目錄下的 commons-logging.properties 檔案中查找org.apache.commons.logging.LogFactory 屬性,如果指定了具體類,嘗試建立執行個體。
  • 以上情況如果都不滿足,則執行個體化預設實作類,即org.apache.commons.logging.impl.LogFactoryImpl。

4.3 SPI 應用案例之 Spring Boot

Spring Boot 是基于 Spring 建構的架構,其設計目的在于簡化 Spring 應用的配置、運作。在 Spring Boot 中,大量運用了自動裝配來盡可能減少配置。

下面是一個 Spring Boot 入口示例,可以看到,代碼非常簡潔。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @GetMapping("/hello")
    public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
        return String.format("Hello %s!", name);
    }
}      

那麼,Spring Boot 是如何做到寥寥幾行代碼,就可以運作一個 Spring Boot 應用的呢。我們不妨帶着疑問,從源碼入手,一步步探究其原理。

4.3.1 @SpringBootApplication 注解

首先,Spring Boot 應用的啟動類上都會标記一個

@SpringBootApplication 注解。

@SpringBootApplication 注解定義如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    // 略
}      

除了 @Target、 @Retention、@Documented、@Inherited 這幾個元注解, 

@SpringBootApplication 注解的定義中還标記了 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 三個注解。

4.3.2 @SpringBootConfiguration 注解

從@SpringBootConfiguration 注解的定義來看,@SpringBootConfiguration 注解本質上就是一個 @Configuration 注解,這意味着被@SpringBootConfiguration 注解修飾的類會被 Spring Boot 識别為一個配置類。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}      
4.3.3 @EnableAutoConfiguration 注解

@EnableAutoConfiguration 注解定義如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}      

@EnableAutoConfiguration 注解包含了 @AutoConfigurationPackage與 @Import({AutoConfigurationImportSelector.class}) 兩個注解。

4.3.4 @AutoConfigurationPackage 注解

@AutoConfigurationPackage 會将被修飾的類作為主配置類,該類所在的 package 會被視為根路徑,Spring Boot 預設會自動掃描根路徑下的所有 Spring Bean(被 @Component 以及繼承 @Component 的各個注解所修飾的類)。——這就是為什麼 Spring Boot 的啟動類一般要置于根路徑的原因。這個功能等同于在 Spring xml 配置中通過 context:component-scan 來指定掃描路徑。@Import 注解的作用是向 Spring 容器中直接注入指定元件。@AutoConfigurationPackage 注解中注明了@Import({Registrar.class})。Registrar 類用于儲存 Spring Boot 的入口類、根路徑等資訊。

4.3.5 SpringFactoriesLoader.loadFactoryNames 方法

@Import(AutoConfigurationImportSelector.class) 表示直接注入AutoConfigurationImportSelector。

AutoConfigurationImportSelector 有一個核心方法getCandidateConfigurations 用于擷取候選配置。該方法調用了SpringFactoriesLoader.loadFactoryNames 方法,這個方法即為 Spring Boot SPI 的關鍵,它負責加載所有 META-INF/spring.factories 檔案,加載的過程由 SpringFactoriesLoader 負責。

Spring Boot 的 META-INF/spring.factories 檔案本質上就是一個 properties 檔案,資料内容就是一個個鍵值對。

SpringFactoriesLoader.loadFactoryNames 方法的關鍵源碼:

// spring.factories 檔案的格式為:key=value1,value2,value3
// 周遊所有 META-INF/spring.factories 檔案
// 解析檔案,獲得 key=factoryClass 的類名稱
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
  String factoryTypeName = factoryType.getName();
  return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
  // 嘗試擷取緩存,如果緩存中有資料,直接傳回
  MultiValueMap<String, String> result = cache.get(classLoader);
  if (result != null) {
    return result;
  }

  try {
    // 擷取資源檔案路徑
    Enumeration<URL> urls = (classLoader != null ?
        classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
        ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    result = new LinkedMultiValueMap<>();
    // 周遊所有路徑
    while (urls.hasMoreElements()) {
      URL url = urls.nextElement();
      UrlResource resource = new UrlResource(url);
      // 解析檔案,得到對應的一組 Properties
      Properties properties = PropertiesLoaderUtils.loadProperties(resource);
      // 周遊解析出的 properties,組裝資料
      for (Map.Entry<?, ?> entry : properties.entrySet()) {
        String factoryTypeName = ((String) entry.getKey()).trim();
        for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
          result.add(factoryTypeName, factoryImplementationName.trim());
        }
      }
    }
    cache.put(classLoader, result);
    return result;
  }
  catch (IOException ex) {
    throw new IllegalArgumentException("Unable to load factories from location [" +
        FACTORIES_RESOURCE_LOCATION + "]", ex);
  }
}      

歸納上面的方法,主要作了這些事:

加載所有 META-INF/spring.factories 檔案,加載過程有 SpringFactoriesLoader 負責。

  • 在 CLASSPATH 中搜尋所有 META-INF/spring.factories 配置檔案。
  • 然後,解析 spring.factories 檔案,擷取指定自動裝配類的全限定名。
4.3.6 Spring Boot 的 AutoConfiguration 類

Spring Boot 有各種 starter 包,可以根據實際項目需要,按需取材。在項目開發中,隻要将 starter 包引入,我們就可以用很少的配置,甚至什麼都不配置,即可擷取相關的能力。通過前面的 Spring Boot SPI 流程,隻完成了自動裝配工作的一半,剩下的工作如何處理呢 ?

以 spring-boot-starter-web 的 jar 包為例,檢視其 maven pom,可以看到,它依賴于 spring-boot-starter,所有 Spring Boot 官方 starter 包都會依賴于這個 jar 包。而 spring-boot-starter 又依賴于 spring-boot-autoconfigure,Spring Boot 的自動裝配秘密,就在于這個 jar 包。

從 spring-boot-autoconfigure 包的結構來看,它有一個 META-INF/spring.factories ,顯然利用了 Spring Boot SPI,來自動裝配其中的配置類。

源碼級深度了解 Java SPI

下圖是 spring-boot-autoconfigure 的 META-INF/spring.factories 檔案的部分内容,可以看到其中注冊了一長串會被自動加載的 AutoConfiguration 類。

源碼級深度了解 Java SPI

以 RedisAutoConfiguration 為例,這個配置類中,會根據 @ConditionalXXX 中的條件去決定是否執行個體化對應的 Bean,執行個體化 Bean 所依賴的重要參數則通過 RedisProperties 傳入。

源碼級深度了解 Java SPI

RedisProperties 中維護了 Redis 連接配接所需要的關鍵屬性,隻要在 yml 或 properties 配置檔案中,指定 spring.redis 開頭的屬性,都會被自動裝載到 RedisProperties 執行個體中。

源碼級深度了解 Java SPI

通過以上分析,已經一步步解讀出 Spring Boot 自動裝載的原理。

五、SPI 應用案例之 Dubbo

Dubbo 并未使用 Java SPI,而是自己封裝了一套新的 SPI 機制。Dubbo SPI 所需的配置檔案需放置在 META-INF/dubbo 路徑下,配置内容形式如下:

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee      

與 Java SPI 實作類配置不同,Dubbo SPI 是通過鍵值對的方式進行配置,這樣可以按需加載指定的實作類。Dubbo SPI 除了支援按需加載接口實作類,還增加了 IOC 和 AOP 等特性。

5.1 ExtensionLoader 入口

Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,可以加載指定的實作類。

ExtensionLoader 的 getExtension 方法是其入口方法,其源碼如下:

public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    if ("true".equals(name)) {
        // 擷取預設的拓展實作類
        return getDefaultExtension();
    }
    // Holder,顧名思義,用于持有目标對象
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances.get(name);
    }
    Object instance = holder.get();
    // 雙重檢查
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // 建立拓展執行個體
                instance = createExtension(name);
                // 設定執行個體到 holder 中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}      

可以看出,這個方法的作用就是:首先檢查緩存,緩存未命中則調用 createExtension 方法建立拓展對象。那麼,createExtension 是如何建立拓展對象的呢,其源碼如下:

private T createExtension(String name) {
    // 從配置檔案中加載所有的拓展類,可得到“配置項名稱”到“配置類”的映射關系表
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            // 通過反射建立執行個體
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // 向執行個體中注入依賴
        injectExtension(instance);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            // 循環建立 Wrapper 執行個體
            for (Class<?> wrapperClass : wrapperClasses) {
                // 将目前 instance 作為參數傳給 Wrapper 的構造方法,并通過反射建立 Wrapper 執行個體。
                // 然後向 Wrapper 執行個體中注入依賴,最後将 Wrapper 執行個體再次指派給 instance 變量
                instance = injectExtension(
                    (T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("...");
    }
}      

createExtension 方法的的工作步驟可以歸納為:

  1. 通過 getExtensionClasses 擷取所有的拓展類
  2. 通過反射建立拓展對象
  3. 向拓展對象中注入依賴
  4. 将拓展對象包裹在相應的 Wrapper 對象中

以上步驟中,第一個步驟是加載拓展類的關鍵,第三和第四個步驟是 Dubbo IOC 與 AOP 的具體實作。

5.2 擷取所有的拓展類

Dubbo 在通過名稱擷取拓展類之前,首先需要根據配置檔案解析出拓展項名稱到拓展類的映射關系表(Map<名稱, 拓展類>),之後再根據拓展項名稱從映射關系表中取出相應的拓展類即可。相關過程的代碼分析如下:

private Map<String, Class<?>> getExtensionClasses() {
    // 從緩存中擷取已加載的拓展類
    Map<String, Class<?>> classes = cachedClasses.get();
    // 雙重檢查
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 加載拓展類
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}      

這裡也是先檢查緩存,若緩存未命中,則通過 synchronized 加鎖。加鎖後再次檢查緩存,并判空。此時如果 classes 仍為 null,則通過 loadExtensionClasses 加載拓展類。下面分析 loadExtensionClasses 方法的邏輯。

private Map<String, Class<?>> loadExtensionClasses() {
    // 擷取 SPI 注解,這裡的 type 變量是在調用 getExtensionLoader 方法時傳入的
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            // 對 SPI 注解内容進行切分
            String[] names = NAME_SEPARATOR.split(value);
            // 檢測 SPI 注解内容是否合法,不合法則抛出異常
            if (names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension...");
            }

            // 設定預設名稱,參考 getDefaultExtension 方法
            if (names.length == 1) {
                cachedDefaultName = names[0];
            }
        }
    }

    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    // 加載指定檔案夾下的配置檔案
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    loadDirectory(extensionClasses, DUBBO_DIRECTORY);
    loadDirectory(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}      

loadExtensionClasses 方法總共做了兩件事情,一是對 SPI 注解進行解析,二是調用 loadDirectory 方法加載指定檔案夾配置檔案。SPI 注解解析過程比較簡單,無需多說。下面我們來看一下 loadDirectory 做了哪些事情。

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
    // fileName = 檔案夾路徑 + type 全限定名
    String fileName = dir + type.getName();
    try {
        Enumeration<java.net.URL> urls;
        ClassLoader classLoader = findClassLoader();
        // 根據檔案名加載所有的同名檔案
        if (classLoader != null) {
            urls = classLoader.getResources(fileName);
        } else {
            urls = ClassLoader.getSystemResources(fileName);
        }
        if (urls != null) {
            while (urls.hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                // 加載資源
                loadResource(extensionClasses, classLoader, resourceURL);
            }
        }
    } catch (Throwable t) {
        logger.error("...");
    }
}      

loadDirectory 方法先通過 classLoader 擷取所有資源連結,然後再通過 loadResource 方法加載資源。我們繼續跟下去,看一下 loadResource 方法的實作。

private void loadResource(Map<String, Class<?>> extensionClasses,
  ClassLoader classLoader, java.net.URL resourceURL) {
    try {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(resourceURL.openStream(), "utf-8"));
        try {
            String line;
            // 按行讀取配置内容
            while ((line = reader.readLine()) != null) {
                // 定位 # 字元
                final int ci = line.indexOf('#');
                if (ci >= 0) {
                    // 截取 # 之前的字元串,# 之後的内容為注釋,需要忽略
                    line = line.substring(0, ci);
                }
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        String name = null;
                        int i = line.indexOf('=');
                        if (i > 0) {
                            // 以等于号 = 為界,截取鍵與值
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            // 加載類,并通過 loadClass 方法對類進行緩存
                            loadClass(extensionClasses, resourceURL,
                                      Class.forName(line, true, classLoader), name);
                        }
                    } catch (Throwable t) {
                        IllegalStateException e = new IllegalStateException("Failed to load extension class...");
                    }
                }
            }
        } finally {
            reader.close();
        }
    } catch (Throwable t) {
        logger.error("Exception when load extension class...");
    }
}      
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL,
    Class<?> clazz, String name) throws NoSuchMethodException {

    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("...");
    }

    // 檢測目标類上是否有 Adaptive 注解
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        if (cachedAdaptiveClass == null) {
            // 設定 cachedAdaptiveClass緩存
            cachedAdaptiveClass = clazz;
        } else if (!cachedAdaptiveClass.equals(clazz)) {
            throw new IllegalStateException("...");
        }

    // 檢測 clazz 是否是 Wrapper 類型
    } else if (isWrapperClass(clazz)) {
        Set<Class<?>> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            wrappers = cachedWrapperClasses;
        }
        // 存儲 clazz 到 cachedWrapperClasses 緩存中
        wrappers.add(clazz);

    // 程式進入此分支,表明 clazz 是一個普通的拓展類
    } else {
        // 檢測 clazz 是否有預設的構造方法,如果沒有,則抛出異常
        clazz.getConstructor();
        if (name == null || name.length() == 0) {
            // 如果 name 為空,則嘗試從 Extension 注解中擷取 name,或使用小寫的類名作為 name
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("...");
            }
        }
        // 切分 name
        String[] names = NAME_SEPARATOR.split(name);
        if (names != null && names.length > 0) {
            Activate activate = clazz.getAnnotation(Activate.class);
            if (activate != null) {
                // 如果類上有 Activate 注解,則使用 names 數組的第一個元素作為鍵,
                // 存儲 name 到 Activate 注解對象的映射關系
                cachedActivates.put(names[0], activate);
            }
            for (String n : names) {
                if (!cachedNames.containsKey(clazz)) {
                    // 存儲 Class 到名稱的映射關系
                    cachedNames.put(clazz, n);
                }
                Class<?> c = extensionClasses.get(n);
                if (c == null) {
                    // 存儲名稱到 Class 的映射關系
                    extensionClasses.put(n, clazz);
                } else if (c != clazz) {
                    throw new IllegalStateException("...");
                }
            }
        }
    }
}      
  • ​​Java SPI 思想梳理​​
  • ​​Dubbo SPI​​
  • ​​springboot 中 SPI 機制​​
  • ​​SpringBoot 的自動裝配原理、自定義 starter 與 spi 機制,一網打盡​​