1. 程式人生 > 程式設計 >Java SPI 機制及其實現

Java SPI 機制及其實現

前言

第一次接觸 SPI 是在看《Java 核心計算卷》中 JDBC 相關的章節的時候,當時看到說在高版本的 JDBC 中可以省略通過 Class.forName 載入驅動這一步, 因為高版本的 JDBC 可以通過 SPI 機制自動載入註冊驅動。

當時看到的時候感覺很驚喜,終於不用寫那又臭又長的 try-catch 了。

後來在閱讀原始碼的過程中又發現 Spring 中也實現了類似於 Java SPI 機制的功能,研究了一下後發現 SPI 機制無論是在使用上還是在實現上,都是很簡單的。

所以,我覺得,可以整一篇部落格總結一下。

隱藏內容

上一次寫部落格還是 6 月 22 號,斷更了 100 多天,感覺有點手生 @_@

ServiceLoader

SPI 的全稱為 (Service Provider Interface),是 JDK 內建的一種服務提供發現機制。主要由工具類 java.util.ServiceLoader 提供相應的支援。

其中的兩個主要角色為:

  • Service - 服務,通常為一個介面或一個抽象類,具體類雖然也可以,但是一般不建議那樣做
  • Service Provider - 服務提供者,服務的具體實現類

使用時,需要在 META-INF/services 下建立和服務的 全限定名

相同的檔案,然後在該檔案中寫入 服務提供者 的全限定名,可以用 # 作為註釋。比如說, 我們可以在檔案 mysql-connector-java/META-INF/services/java.sql.Driver 中發現如下內容:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
複製程式碼

然後,就可以通過 ServiceLoader 來獲取這些服務提供者。由於 ServiceLoader 並沒有提供直接獲取服務提供者的方法,因此,只能通過迭代的方式獲取:

ServiceLoader<Service> loader = ServiceLoader.load(Service.class);

for
(Service service : loader) { // ... } 複製程式碼

可以看到,ServiceLoader 的使用還是很簡單的,更多的和 ServiceLoader 相關的內容可以看一下官方檔案:ServiceLoader (Java Platform SE 8 )

JDBC 中的使用

如果要找一個使用了 SPI 機制的例子的話,最直接的就是 JDBC 中通過 SPI 的方式載入驅動了,這裡可以看一下 JDBC 的使用方式:

public class DriverManager {
  static {
    loadInitialDrivers();
  }

  private static void loadInitialDrivers() {
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

          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;
        }
      });
  }
}
複製程式碼

通過上面的簡化過後的程式碼可以發現,在載入 DriverManager 這個類的時候就會通過靜態初始化程式碼塊呼叫執行 loadInitialDrivers 方法,而這個方法會通過 ServiceLoader 載入所有的 Driver 提供者。

而在相應的 Driver 提供類中,比如類 com.mysql.jdbc.Driver 中就存在如下形式的程式碼:

static {
  try {
    java.sql.DriverManager.registerDriver(new Driver());
  } catch (SQLException E) {
    throw new RuntimeException("Can't register driver!");
  }
}
複製程式碼

是不是很簡單?載入 DriverManager 的時候通過 SPI 機制載入各個 Driver,然後各個 Driver 又在它們自己的靜態初始化程式碼塊中將自己註冊到 DriverManager。

更多的使用場景

通過 JDBC 中 SPI 機制的使用可以發現,要使用 SPI 的話還是很簡單的,那麼,我們可以在什麼地方使用 SPI 呢?

由於 SPI 機制的限制,單個 ServiceLoader 只能載入單個型別的 Service,同時還必須建立相應的檔案放到 META-INF/services 目錄下,因此,使用場景最好就是類似 JDBC 中這種, 可以通過單個物件來訪問其他服務提供者的場景,即:可以使用 門面模式 的場景。

比如說,現在 Java 中存在不少常用的 JSON 庫,比如 Gson、FastJSON、Jackson 等,這些庫在使用時都可以通過簡單的封裝來滿足大部分的需求,那麼, 我們就可以考慮通過 SPI 機制來實現一個這些 JSON 庫的門面,將 JSON 的處理下放到 Service Provider 來完成,而我們通過門面來使用這些服務。

這樣一來,我們一方面可以提供自己的預設實現,也可以留出擴充套件的介面,也就不需要自己手動去載入那些實現了。

實現原理

SPI 不僅在使用上很簡單,它的實現原理也很簡單,關鍵就在 ClassLoader.getResources 這個方法上,SPI 載入服務的方式就是通過 ClassLoader.getResources 方法找到 META-INF/services 目錄下的相應檔案, 然後解析檔案得到服務提供者的類名。

最後通過 Class.forName() -> clazz.newInstance() 得到例項返回。

非常簡單且直白的實現方式,比較值得注意的就是 ClassLoader.getResources 方法的使用了,比如,你可以在一個 Spring 專案下執行如下程式碼:

public class Test {
  public static void main(String[] args) throws Exception {
    Enumeration<URL> urls = Test.class.getClassLoader().getResources("META-INF/spring.factories");
    while (urls.hasMoreElements()) {
      System.out.println(urls.nextElement());
    }
  }
}
複製程式碼

這個就是 Spring 中通過 SpringFactoriesLoader 來載入相關的類的起點。

SpringFactoriesLoader

SpringFactoriesLoader 是 Spring 中十分重要的一個擴充套件機制之一,它的使用方式和實現原理和 SPI 十分相似,只不過,提供了更加強大的功能。

和 SPI 不同,由於 SpringFactoriesLoader 中的配置檔案格式是 properties 檔案,因此,不需要要像 SPI 中那樣為每個服務都建立一個檔案, 而是選擇直接把所有服務都扔到 META-INF/spring.factories 檔案中。

比如,spring-boot-autoconfigure 中的部分內容:

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnClassCondition

# ...
複製程式碼

更多的使用可以參考:SpringFactoriesLoader (Spring Framework 5.2.0.RELEASE API)

結語

總的來說,無論是 ServiceLoader 還是 SpringFactoriesLoader,它們的基本原理都是一樣的,都是通過 ClassLoader.getResources 方法找到相應的配置檔案, 然後解析檔案得到服務提供者的全限定名。

得益於 Java 強大的反射機制,拿到全限定名後基本上就可以為所欲為了 @_@

隱藏內容

簡陋的 JSON 門面:DefaultJsonProviderFactory.java