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