SPI 是一種用于動態(tài)加載服務(wù)的機制。它的核心思想就是解耦,屬于典型的微內(nèi)核架構(gòu)模式。SPI 在 Java 世界應(yīng)用非常廣泛,如:Dubbo、Spring Boot 等框架。本文從源碼入手分析,深入探討 Java SPI 的特性、原理,以及在一些比較經(jīng)典領(lǐng)域的應(yīng)用。
一、SPI 簡介
SPI 全稱 Service Provider Interface,是 Java 提供的,旨在由第三方實現(xiàn)或擴展的 API,它是一種用于動態(tài)加載服務(wù)的機制。Java 中 SPI 機制主要思想是將裝配的控制權(quán)移到程序之外,在模塊化設(shè)計中這個機制尤其重要,其核心思想就是解耦。
Java SPI 有四個要素:
-
SPI 接口:為服務(wù)提供者實現(xiàn)類約定的的接口或抽象類。
-
SPI 實現(xiàn)類:實際提供服務(wù)的實現(xiàn)類。
-
SPI 配置:Java SPI 機制約定的配置文件,提供查找服務(wù)實現(xiàn)類的邏輯。配置文件必須置于 META-INF/services 目錄中,并且,文件名應(yīng)與服務(wù)提供者接口的完全限定名保持一致。文件中的每一行都有一個實現(xiàn)服務(wù)類的詳細(xì)信息,同樣是服務(wù)提供者類的完全限定名稱。
-
ServiceLoader:Java SPI 的核心類,用于加載 SPI 實現(xiàn)類。ServiceLoader 中有各種實用方法來獲取特定實現(xiàn)、迭代它們或重新加載服務(wù)。
二、SPI 示例
正所謂,實踐出真知,我們不妨通過一個具體的示例來看一下,如何使用 Java SPI。
2.1 SPI 接口
首先,需要定義一個 SPI 接口,和普通接口并沒有什么差別。
package io.github.dunwu.javacore.spi;
public interface DataStorage {
String search(String key);
}
2.2 SPI 實現(xiàn)類
假設(shè),我們需要在程序中使用兩種不同的數(shù)據(jù)存儲——MySQL 和 Redis。因此,我們需要兩個不同的實現(xiàn)類去分別完成相應(yīng)工作。
MySQL查詢 MOCK 類
package io.github.dunwu.javacore.spi;
public class MysqlStorage implements DataStorage {
public String search(String key) {
return "【Mysql】搜索" + key + ",結(jié)果:No";
}
}
Redis 查詢 MOCK 類
package io.github.dunwu.javacore.spi;
public class RedisStorage implements DataStorage {
public String search(String key) {
return "【Redis】搜索" + key + ",結(jié)果:Yes";
}
}
service 傳入的是期望加載的 SPI 接口類型 到目前為止,定義接口,并實現(xiàn)接口和普通的 Java 接口實現(xiàn)沒有任何不同。
2.3 SPI 配置
如果想通過 Java SPI 機制來發(fā)現(xiàn)服務(wù),就需要在 SPI 配置中約定好發(fā)現(xiàn)服務(wù)的邏輯。配置文件必須置于 META-INF/services 目錄中,并且,文件名應(yīng)與服務(wù)提供者接口的完全限定名保持一致。文件中的每一行都有一個實現(xiàn)服務(wù)類的詳細(xì)信息,同樣是服務(wù)提供者類的完全限定名稱。以本示例代碼為例,其文件名應(yīng)該為
io.github.dunwu.javacore.spi.DataStorage,
文件中的內(nèi)容如下:
io.github.dunwu.javacore.spi.MysqlStorage
io.github.dunwu.javacore.spi.RedisStorage
2.4 ServiceLoader
完成了上面的步驟,就可以通過 ServiceLoader 來加載服務(wù)。示例如下:
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,結(jié)果:No
【Redis】搜索Yes Or No,結(jié)果:Yes
三、SPI 原理
上文中,我們已經(jīng)了解 Java SPI 的要素以及使用 Java SPI 的方法。你有沒有想過,Java SPI 和普通 Java 接口有何不同,Java SPI 是如何工作的。實際上,Java SPI 機制依賴于 ServiceLoader 類去解析、加載服務(wù)。因此,掌握了 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 服務(wù)
private final Class service;
// 用于加載 SPI 服務(wù)的類加載器
private final ClassLoader loader;
// ServiceLoader 創(chuàng)建時的訪問控制上下文
private final AccessControlContext acc;
// SPI 服務(wù)緩存,按實例化的順序排列
private LinkedHashMap providers = new LinkedHashMap<>();
// 懶查詢迭代器
private LazyIterator lookupIterator;
// ...
}
3.2 ServiceLoader 的工作流程
(1)ServiceLoader.load靜態(tài)方法
應(yīng)用程序加載 Java SPI 服務(wù),都是先調(diào)用 ServiceLoader.load 靜態(tài)方法。
ServiceLoader.load 靜態(tài)方法的作用是:
① 指定類加載 ClassLoader 和訪問控制上下文;
② 然后,重新加載 SPI 服務(wù)
-
清空緩存中所有已實例化的 SPI 服務(wù)
-
根據(jù)ClassLoader和 SPI 類型,創(chuàng)建懶加載迭代器
這里,摘錄 ServiceLoader.load 相關(guān)源碼,如下:
// service 傳入的是期望加載的 SPI 接口類型
// loader 是用于加載 SPI 服務(wù)的類加載器
public static ServiceLoader load(Class service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
public void reload() {
// 清空緩存中所有已實例化的 SPI 服務(wù)
providers.clear();
// 根據(jù) ClassLoader 和 SPI 類型,創(chuàng)建懶加載迭代器
lookupIterator = new LazyIterator(service, loader);
}
// 私有構(gòu)造方法
// 重新加載 SPI 服務(wù)
private ServiceLoader(Class 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 服務(wù)
reload();
}
(2)應(yīng)用程序通過ServiceLoader的iterator方法遍歷 SPI 實例
ServiceLoader 的類定義,明確了 ServiceLoader 類實現(xiàn)了 Iterable
當(dāng)應(yīng)用程序調(diào)用 ServiceLoader 的 iterator 方法時,ServiceLoader 會先判斷緩存 providers 中是否有數(shù)據(jù):如果有,則直接返回緩存 providers 的迭代器;如果沒有,則返回懶加載迭代器的迭代器。
public Iterator iterator() {
return new Iterator() {
// 緩存 SPI providers
Iterator> 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關(guān)鍵代碼
hasNextService 方法:
-
拼接META-INF/services/+ SPI 接口全限定名
-
通過類加載器,嘗試加載資源文件
-
解析資源文件中的內(nèi)容,獲取 SPI 接口的實現(xiàn)類的全限定名nextName
nextService 方法:
-
hasNextService()方法解析出了 SPI 實現(xiàn)類的的全限定名 nextName,通過反射,獲取 SPI 實現(xiàn)類的類定義 Class。
-
然后,嘗試通過 Class 的 newInstance 方法實例化一個 SPI 服務(wù)對象。如果成功,則將這個對象加入到緩存 providers 中并返回該對象。
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 1.拼接 META-INF/services/ + SPI 接口全限定名
// 2.通過類加載器,嘗試加載資源文件
// 3.解析資源文件中的內(nèi)容
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 和類加載器
通過上面兩個章節(jié)中,走讀 ServiceLoader 代碼,我們已經(jīng)大致了解 Java SPI 的工作原理,即通過 ClassLoader 加載 SPI 配置文件,解析 SPI 服務(wù),然后通過反射,實例化 SPI 服務(wù)實例。我們不妨思考一下,為什么加載 SPI 服務(wù)時,需要指定類加載器 ClassLoader 呢?
學(xué)習(xí)過 JVM 的讀者,想必都了解過類加載器的雙親委派模型(Parents Delegation Model)。雙親委派模型要求除了頂層的 BootstrapClassLoader 外,其余的類加載器都應(yīng)有自己的父類加載器。這里類加載器之間的父子關(guān)系一般通過組合(Composition)關(guān)系來實現(xiàn),而不是通過繼承(Inheritance)的關(guān)系實現(xiàn)。
雙親委派機制約定了:一個類加載器首先將類加載請求傳送到父類加載器,只有當(dāng)父類加載器無法完成類加載請求時才嘗試加載。
雙親委派的好處:使得 Java 類伴隨著它的類加載器,天然具備一種帶有優(yōu)先級的層次關(guān)系,從而使得類加載得到統(tǒng)一,不會出現(xiàn)重復(fù)加載的問題:
-
系統(tǒng)類防止內(nèi)存中出現(xiàn)多份同樣的字節(jié)碼
-
保證 Java 程序安全穩(wěn)定運行
例如:java.lang.Object 存放在 rt.jar 中,如果編寫另外一個 java.lang.Object 的類并放到 classpath 中,程序可以編譯通過。因為雙親委派模型的存在,所以在 rt.jar 中的 Object 比在 classpath 中的 Object 優(yōu)先級更高,因為 rt.jar 中的 Object 使用的是啟動類加載器,而 classpath 中的 Object 使用的是應(yīng)用程序類加載器。正因為 rt.jar 中的 Object 優(yōu)先級更高,因為程序中所有的 Object 都是這個 Object。
雙親委派的限制:子類加載器可以使用父類加載器已經(jīng)加載的類,而父類加載器無法使用子類加載器已經(jīng)加載的。——這就導(dǎo)致了雙親委派模型并不能解決所有的類加載器問題。Java SPI 就面臨著這樣的問題:
-
SPI 的接口是 Java 核心庫的一部分,是由 BootstrapClassLoader 加載的;
-
而 SPI 實現(xiàn)的 Java 類一般是由 AppClassLoader 來加載的。BootstrapClassLoader 是無法找到 SPI 的實現(xiàn)類的,因為它只加載 Java 的核心庫。它也不能代理給 AppClassLoader,因為它是最頂層的類加載器。這也解釋了本節(jié)開始的問題——為什么加載 SPI 服務(wù)時,需要指定類加載器 ClassLoader 呢?因為如果不指定 ClassLoader,則無法獲取 SPI 服務(wù)。
如果不做任何的設(shè)置,Java 應(yīng)用的線程的上下文類加載器默認(rèn)就是 AppClassLoader。在核心類庫使用 SPI 接口時,傳遞的類加載器使用線程上下文類加載器,就可以成功的加載到 SPI 實現(xiàn)的類。線程上下文類加載器在很多 SPI 的實現(xiàn)中都會用到。
通常可以通過
Thread.currentThread().getClassLoader()
和 Thread.currentThread().getContextClassLoader()獲取線程上下文類加載器。
3.4 Java SPI 的不足
Java SPI 存在一些不足:
-
不能按需加載,需要遍歷所有的實現(xiàn),并實例化,然后在循環(huán)中才能找到我們需要的實現(xiàn)。如果不想用某些實現(xiàn)類,或者某些類實例化很耗時,它也被載入并實例化了,這就造成了浪費。
-
獲取某個實現(xiàn)類的方式不夠靈活,只能通過 Iterator 形式獲取,不能根據(jù)某個參數(shù)來獲取對應(yīng)的實現(xiàn)類。
-
多個并發(fā)多線程使用 ServiceLoader 類的實例是不安全的。
四、SPI 應(yīng)用場景
SPI 在 Java 開發(fā)中應(yīng)用十分廣泛。首先,在 Java 的 java.util.spi package 中就約定了很多 SPI 接口。下面,列舉一些 SPI 接口:
-
TimeZoneNameProvider:為 TimeZone 類提供本地化的時區(qū)名稱。
-
DateFormatProvider:為指定的語言環(huán)境提供日期和時間格式。
-
NumberFormatProvider:為 NumberFormat 類提供貨幣、整數(shù)和百分比值。
-
Driver:從 4.0 版開始,JDBC API 支持 SPI 模式。舊版本使用 Class.forName() 方法加載驅(qū)動程序。
-
PersistenceProvider:提供 JPA API 的實現(xiàn)。
-
等等
除此以外,SPI 還有很多應(yīng)用,下面列舉幾個經(jīng)典案例。
4.1 SPI 應(yīng)用案例之 JDBC DriverManager
作為 Java 工程師,尤其是 CRUD 工程師,相必都非常熟悉 JDBC。眾所周知,關(guān)系型數(shù)據(jù)庫有很多種,如:MySQL、Oracle、PostgreSQL 等等。JDBC 如何識別各種數(shù)據(jù)庫的驅(qū)動呢?
4.1.1創(chuàng)建數(shù)據(jù)庫連接
我們先回顧一下,JDBC 如何創(chuàng)建數(shù)據(jù)庫連接的呢?
在JDBC4.0 之前,連接數(shù)據(jù)庫的時候,通常會用 Class.forName(XXX)方法來加載數(shù)據(jù)庫相應(yīng)的驅(qū)動,然后再獲取數(shù)據(jù)庫連接,繼而進行 CRUD 等操作。
Class.forName("com.mysql.jdbc.Driver")
而 JDBC4.0 之后,不再需要用
Class.forName(XXX)方法來加載數(shù)據(jù)庫驅(qū)動,直接獲取連接就可以了。顯然,這種方式很方便,但是如何做到的呢?
(1)JDBC 接口:首先,Java 中內(nèi)置了接口 java.sql.Driver。
(2)JDBC 接口實現(xiàn):各個數(shù)據(jù)庫的驅(qū)動自行實現(xiàn) java.sql.Driver 接口,用于管理數(shù)據(jù)庫連接。
① MySQL:在 MySQL的 Java 驅(qū)動包 mysql-connector-java-XXX.jar 中,可以找到 META-INF/services 目錄,該目錄下會有一個名字為java.sql.Driver 的文件,文件內(nèi)容是com.mysql.cj.jdbc.Driver。
com.mysql.cj.jdbc.Driver 正是 MySQL 版的 java.sql.Driver 實現(xiàn)。如下圖所示:
②PostgreSQL 實現(xiàn):在 PostgreSQL 的 Java 驅(qū)動包 postgresql-42.0.0.jar 中,也可以找到同樣的配置文件,文件內(nèi)容是 org.postgresql.Driver,org.postgresql.Driver 正是 PostgreSQL 版的 java.sql.Driver 實現(xiàn)。
(3)創(chuàng)建數(shù)據(jù)庫連接
以 MySQL 為例,創(chuàng)建數(shù)據(jù)庫連接代碼如下:
final String DB_URL = String.format("jdbc//%s:%s/%s", DB_HOST, DB_PORT, DB_SCHEMA);
connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
4.1.2 DriverManager
從前文,我們已經(jīng)知道 DriverManager 是創(chuàng)建數(shù)據(jù)庫連接的關(guān)鍵。它究竟是如何工作的呢?
可以看到是加載實例化驅(qū)動的,接著看 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 獲取所有實現(xiàn) java.sql.Driver 的驅(qū)動類
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
// 利用 SPI,記載所有 Driver 服務(wù)
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
// 獲取迭代器
Iterator driversIterator = loadedDrivers.iterator();
try{
// 遍歷迭代器
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
// 打印數(shù)據(jù)庫驅(qū)動信息
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);
// 嘗試實例化驅(qū)動
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
上面的代碼主要步驟是:
-
從系統(tǒng)變量中獲取驅(qū)動的實現(xiàn)類。
-
利用 SPI 來獲取所有驅(qū)動的實現(xiàn)類。
-
遍歷所有驅(qū)動,嘗試實例化各個實現(xiàn)類。
-
根據(jù)第 1 步獲取到的驅(qū)動列表來實例化具體的實現(xiàn)類。
需要關(guān)注的是下面這行代碼:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
這里實際獲取的是
java.util.ServiceLoader.LazyIterator 迭代器。調(diào)用其 hasNext 方法時,會搜索 classpath 下以及 jar 包中的 META-INF/services 目錄,查找 java.sql.Driver 文件,并找到文件中的驅(qū)動實現(xiàn)類的全限定名。調(diào)用其 next 方法時,會根據(jù)驅(qū)動類的全限定名去嘗試實例化一個驅(qū)動類的對象。
4.2SPI 應(yīng)用案例之 Common-Loggin
common-logging(也稱 Jakarta Commons Logging,縮寫 JCL)是常用的日志門面工具包。
common-logging 的核心類是入口是 LogFactory,LogFatory 是一個抽象類,它負(fù)責(zé)加載具體的日志實現(xiàn)。
其入口方法是 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 采用了工廠設(shè)計模式,是先調(diào)用 getFactory 方法獲取具體日志庫的工廠類,然后根據(jù)類名稱或類型創(chuàng)建日志實例。
LogFatory.getFactory 方法負(fù)責(zé)選出匹配的日志工廠,其源碼如下:
public static LogFactory getFactory() throws LogConfigurationException {
// 省略...
// 加載 commons-logging.properties 配置文件
Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);
// 省略...
// 決定創(chuàng)建哪個 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 屬性,嘗試實例化具體實現(xiàn)類
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 實現(xiàn)類
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)以上情況都不滿足,實例化默認(rèn)實現(xiàn)類 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,如果指定了具體類,嘗試創(chuàng)建實例。
-
利用 Java SPI 機制,嘗試在 classpatch 的 META-INF/services 目錄下尋找
org.apache.commons.logging.LogFactory 的實現(xiàn)類。
-
嘗試從 classpath 目錄下的 commons-logging.properties 文件中查找
org.apache.commons.logging.LogFactory 屬性,如果指定了具體類,嘗試創(chuàng)建實例。
-
以上情況如果都不滿足,則實例化默認(rèn)實現(xiàn)類,即
org.apache.commons.logging.impl.LogFactoryImpl。
4.3 SPI 應(yīng)用案例之 Spring Boot
Spring Boot 是基于 Spring 構(gòu)建的框架,其設(shè)計目的在于簡化 Spring 應(yīng)用的配置、運行。在 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;
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
public String hello( String name) {
return String.format("Hello %s!", name);
}
}
那么,Spring Boot 是如何做到寥寥幾行代碼,就可以運行一個 Spring Boot 應(yīng)用的呢。我們不妨帶著疑問,從源碼入手,一步步探究其原理。
4.3.1 @SpringBootApplication 注解
首先,Spring Boot 應(yīng)用的啟動類上都會標(biāo)記一個
@SpringBootApplication 注解。
@SpringBootApplication 注解定義如下:
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
),
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public SpringBootApplication {
// 略
}
除了@Target、@Retention、@Documented、@Inherited 這幾個元注解,
@SpringBootApplication 注解的定義中還標(biāo)記了@SpringBootConfiguration、
@EnableAutoConfiguration、@ComponentScan 三個注解。
4.3.2 @SpringBootConfiguration 注解
從@SpringBootConfiguration 注解的定義來看,@SpringBootConfiguration 注解本質(zhì)上就是一個@Configuration 注解,這意味著被@SpringBootConfiguration 注解修飾的類會被 Spring Boot 識別為一個配置類。
public SpringBootConfiguration {
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
4.3.3 @EnableAutoConfiguration 注解
@EnableAutoConfiguration 注解定義如下:
public 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 默認(rèn)會自動掃描根路徑下的所有 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 用于獲取候選配置。該方法調(diào)用了
SpringFactoriesLoader.loadFactoryNames 方法,這個方法即為 Spring Boot SPI 的關(guān)鍵,它負(fù)責(zé)加載所有 META-INF/spring.factories 文件,加載的過程由 SpringFactoriesLoader 負(fù)責(zé)。
Spring Boot 的 META-INF/spring.factories 文件本質(zhì)上就是一個 properties 文件,數(shù)據(jù)內(nèi)容就是一個個鍵值對。
SpringFactoriesLoader.loadFactoryNames 方法的關(guān)鍵源碼:
// spring.factories 文件的格式為:key=value1,value2,value3
// 遍歷所有 META-INF/spring.factories 文件
// 解析文件,獲得 key=factoryClass 的類名稱
public static List<String> loadFactoryNames(Class> factoryType, ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories( ClassLoader classLoader) {
// 嘗試獲取緩存,如果緩存中有數(shù)據(jù),直接返回
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
// 獲取資源文件路徑
Enumeration 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);
// 解析文件,得到對應(yīng)的一組 Properties
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 遍歷解析出的 properties,組裝數(shù)據(jù)
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 負(fù)責(zé)。
-
在 CLASSPATH 中搜尋所有 META-INF/spring.factories 配置文件。
-
然后,解析 spring.factories 文件,獲取指定自動裝配類的全限定名。
4.3.6 Spring Boot 的 AutoConfiguration 類
Spring Boot 有各種 starter 包,可以根據(jù)實際項目需要,按需取材。在項目開發(fā)中,只要將 starter 包引入,我們就可以用很少的配置,甚至什么都不配置,即可獲取相關(guān)的能力。通過前面的 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 包的結(jié)構(gòu)來看,它有一個 META-INF/spring.factories ,顯然利用了 Spring Boot SPI,來自動裝配其中的配置類。
下圖是 spring-boot-autoconfigure 的 META-INF/spring.factories 文件的部分內(nèi)容,可以看到其中注冊了一長串會被自動加載的 AutoConfiguration 類。
以 RedisAutoConfiguration 為例,這個配置類中,會根據(jù)@ConditionalXXX 中的條件去決定是否實例化對應(yīng)的 Bean,實例化 Bean 所依賴的重要參數(shù)則通過 RedisProperties 傳入。
RedisProperties 中維護了 Redis 連接所需要的關(guān)鍵屬性,只要在 yml 或 properties 配置文件中,指定 spring.redis 開頭的屬性,都會被自動裝載到 RedisProperties 實例中。
通過以上分析,已經(jīng)一步步解讀出 Spring Boot 自動裝載的原理。
五、SPI 應(yīng)用案例之 Dubbo
Dubbo 并未使用 Java SPI,而是自己封裝了一套新的 SPI 機制。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下,配置內(nèi)容形式如下:
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
與 Java SPI 實現(xiàn)類配置不同,Dubbo SPI 是通過鍵值對的方式進行配置,這樣可以按需加載指定的實現(xiàn)類。Dubbo SPI 除了支持按需加載接口實現(xiàn)類,還增加了 IOC 和 AOP 等特性。
5.1 ExtensionLoader 入口
Dubbo SPI 的相關(guān)邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,可以加載指定的實現(xiàn)類。
ExtensionLoader 的 getExtension 方法是其入口方法,其源碼如下:
public T getExtension(String name) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Extension name == null");
if ("true".equals(name)) {
// 獲取默認(rèn)的拓展實現(xiàn)類
return getDefaultExtension();
}
// Holder,顧名思義,用于持有目標(biāo)對象
Holder
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder
holder = cachedInstances.get(name);
}
Object instance = holder.get();
// 雙重檢查
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 創(chuàng)建拓展實例
instance = createExtension(name);
// 設(shè)置實例到 holder 中
holder.set(instance);
}
}
}
return (T) instance;
}
可以看出,這個方法的作用就是:首先檢查緩存,緩存未命中則調(diào)用 createExtension 方法創(chuàng)建拓展對象。那么,createExtension 是如何創(chuàng)建拓展對象的呢,其源碼如下:
private T createExtension(String name) {
// 從配置文件中加載所有的拓展類,可得到“配置項名稱”到“配置類”的映射關(guān)系表
Class> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
// 通過反射創(chuàng)建實例
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 向?qū)嵗凶⑷胍蕾?/span>
injectExtension(instance);
Set> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
// 循環(huán)創(chuàng)建 Wrapper 實例
for (Class> wrapperClass : wrapperClasses) {
// 將當(dāng)前 instance 作為參數(shù)傳給 Wrapper 的構(gòu)造方法,并通過反射創(chuàng)建 Wrapper 實例。
// 然后向 Wrapper 實例中注入依賴,最后將 Wrapper 實例再次賦值給 instance 變量
instance = injectExtension(
(T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("...");
}
}
createExtension 方法的的工作步驟可以歸納為:
-
通過getExtensionClasses獲取所有的拓展類
-
通過反射創(chuàng)建拓展對象
-
向拓展對象中注入依賴
-
將拓展對象包裹在相應(yīng)的Wrapper對象中
以上步驟中,第一個步驟是加載拓展類的關(guān)鍵,第三和第四個步驟是 Dubbo IOC 與 AOP 的具體實現(xiàn)。
5.2獲取所有的拓展類
Dubbo 在通過名稱獲取拓展類之前,首先需要根據(jù)配置文件解析出拓展項名稱到拓展類的映射關(guān)系表(Map<名稱, 拓展類>),之后再根據(jù)拓展項名稱從映射關(guān)系表中取出相應(yīng)的拓展類即可。相關(guān)過程的代碼分析如下:
private Map> getExtensionClasses() {
// 從緩存中獲取已加載的拓展類
Map> 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 變量是在調(diào)用 getExtensionLoader 方法時傳入的
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
// 對 SPI 注解內(nèi)容進行切分
String[] names = NAME_SEPARATOR.split(value);
// 檢測 SPI 注解內(nèi)容是否合法,不合法則拋出異常
if (names.length > 1) {
throw new IllegalStateException("more than 1 default extension name on extension...");
}
// 設(shè)置默認(rèn)名稱,參考 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 注解進行解析,二是調(diào)用 loadDirectory 方法加載指定文件夾配置文件。SPI 注解解析過程比較簡單,無需多說。下面我們來看一下 loadDirectory 做了哪些事情。
private void loadDirectory(Map<String, Class>> extensionClasses, String dir) {
// fileName = 文件夾路徑 + type 全限定名
String fileName = dir + type.getName();
try {
Enumeration urls;
ClassLoader classLoader = findClassLoader();
// 根據(jù)文件名加載所有的同名文件
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 方法加載資源。我們繼續(xù)跟下去,看一下 loadResource 方法的實現(xiàn)。
private void loadResource(Map> extensionClasses,
ClassLoader classLoader, java.net.URL resourceURL) {
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(resourceURL.openStream(), "utf-8"));
try {
String line;
// 按行讀取配置內(nèi)容
while ((line = reader.readLine()) != null) {
// 定位 # 字符
final int ci = line.indexOf('#');
if (ci >= 0) {
// 截取 # 之前的字符串,# 之后的內(nèi)容為注釋,需要忽略
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...");
}
}
loadResource 方法用于讀取和解析配置文件,并通過反射加載類,最后調(diào)用 loadClass 方法進行其他操作。loadClass 方法用于主要用于操作緩存,該方法的邏輯如下:
private void loadClass(Map<String, Class>> extensionClasses, java.net.URL resourceURL,
Class> clazz, String name) throws NoSuchMethodException {
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("...");
}
// 檢測目標(biāo)類上是否有 Adaptive 注解
if (clazz.isAnnotationPresent(Adaptive.class)) {
if (cachedAdaptiveClass == null) {
// 設(shè)置 cachedAdaptiveClass緩存
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("...");
}
// 檢測 clazz 是否是 Wrapper 類型
} else if (isWrapperClass(clazz)) {
Set> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet>();
wrappers = cachedWrapperClasses;
}
// 存儲 clazz 到 cachedWrapperClasses 緩存中
wrappers.add(clazz);
// 程序進入此分支,表明 clazz 是一個普通的拓展類
} else {
// 檢測 clazz 是否有默認(rèn)的構(gòu)造方法,如果沒有,則拋出異常
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 數(shù)組的第一個元素作為鍵,
// 存儲 name 到 Activate 注解對象的映射關(guān)系
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
// 存儲 Class 到名稱的映射關(guān)系
cachedNames.put(clazz, n);
}
Class> c = extensionClasses.get(n);
if (c == null) {
// 存儲名稱到 Class 的映射關(guān)系
extensionClasses.put(n, clazz);
} else if (c != clazz) {
throw new IllegalStateException("...");
}
}
}
}
}
如上,loadClass 方法操作了不同的緩存,比如 cachedAdaptiveClass、
cachedWrapperClasses 和 cachedNames 等等。除此之外,該方法沒有其他什么邏輯了。
-
Java SPI 思想梳理
-
Dubbo SPI
-
springboot 中 SPI 機制
-
SpringBoot 的自動裝配原理、自定義 starter 與 spi 機制,一網(wǎng)打盡
審核編輯 :李倩
-
JAVA
+關(guān)注
關(guān)注
19文章
2970瀏覽量
104810 -
SPI
+關(guān)注
關(guān)注
17文章
1707瀏覽量
91670
原文標(biāo)題:源碼級深度理解Java SPI
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論