Java SPI 機制及其實現

前言

第一次接觸 SPI 是在看《Java 核心計算卷》中 JDBC 相關的章節的時候,當時看到說在高版本的 JDBC 中能夠省略經過 Class.forName 加載驅動這一步, 由於高版本的 JDBC 能夠經過 SPI 機制自動加載註冊驅動。html

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

後來在閱讀源碼的過程當中又發現 Spring 中也實現了相似於 Java SPI 機制的功能,研究了一下後發現 SPI 機制不管是在使用上仍是在實現上,都是很簡單的。mysql

因此,我以爲,能夠整一篇博客總結一下。git

隱藏內容

上一次寫博客仍是 6 月 22 號,斷更了 100 多天,感受有點手生 @_@github

ServiceLoader

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

其中的兩個主要角色爲:sql

  • Service - 服務,一般爲一個接口或一個抽象類,具體類雖然也能夠,可是通常不建議那樣作
  • Service Provider - 服務提供者,服務的具體實現類

使用時,須要在 META-INF/services 下建立和服務的 全限定名 相同的文件,而後在該文件中寫入 服務提供者 的全限定名,能夠用 # 做爲註釋。好比說, 咱們能夠在文件 mysql-connector-java/META-INF/services/java.sql.Driver 中發現以下內容:json

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
複製代碼

而後,就能夠經過 ServiceLoader 來獲取這些服務提供者。因爲 ServiceLoader 並無提供直接獲取服務提供者的方法,所以,只能經過迭代的方式獲取:api

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

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

能夠看到,ServiceLoader 的使用仍是很簡單的,更多的和 ServiceLoader 相關的內容能夠看一下官方文檔:ServiceLoader (Java Platform SE 8 )oracle

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

相關文章
相關標籤/搜索