SPI 全稱為 Service Provider Interface
,是一種服務發現機制。
SPI 的本質是將介面實現類的全限定名配置在檔案中,並由服務載入器讀取配置檔案,載入實現類。這樣可以在執行時,動態為介面替換實現類。正因此特性,我們可以很容易的透過 SPI 機制為我們的程式提供擴充功能。
1 Java SPI 示例
本節透過一個示例演示 Java SPI 的使用方法。首先,我們定義一個介面,名稱為 Robot。
public interface Robot {
void sayHello();
}
接下來定義兩個實現類,分別為 OptimusPrime
和 Bumblebee
。
public class OptimusPrime implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Optimus Prime.");
}
}
public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}
接下來 META-INF/services
資料夾下建立一個檔案,名稱為 Robot 的全限定名 org.apache.spi.Robot
。檔案內容為實現類的全限定的類名,如下:
org.apache.spi.OptimusPrime
org.apache.spi.Bumblebee
做好所需的準備工作,接下來編寫程式碼進行測試。
public class JavaSPITest {
@Test
public void sayHello() throws Exception {
ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
System.out.println("Java SPI");
// 1. forEach 模式
serviceLoader.forEach(Robot::sayHello);
// 2. 迭代器模式
Iterator<Robot> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
Robot robot = iterator.next();
//System.out.println(robot);
//robot.sayHello();
}
}
}
最後來看一下測試結果,如下 :
2 經典 Java SPI 應用 : JDBC DriverManager
在JDBC4.0
之前,我們開發有連線資料庫的時候,通常先載入資料庫相關的驅動,然後再進行獲取連線等的操作。
// STEP 1: Register JDBC driver
Class.forName("com.mysql.jdbc.Driver");
// STEP 2: Open a connection
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password);
JDBC4.0
之後使用了 Java 的 SPI 擴充套件機制,不再需要用 Class.forName("com.mysql.jdbc.Driver")
來載入驅動,直接就可以獲取 JDBC 連線。
接下來,我們來看看應用如何載入 MySQL JDBC 8.0.22 驅動:
首先 DriverManager
類是驅動管理器,也是驅動載入的入口。
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
在 Java 中,static
塊用於靜態初始化,它在類被載入到 Java 虛擬機器中時執行。
靜態塊會載入例項化驅動,接下來我們看看loadInitialDrivers
方法。
載入驅動程式碼包含四個步驟:
-
系統變數中獲取有關驅動的定義。
-
使用 SPI 來獲取驅動的實現類(字串的形式)。
-
遍歷使用 SPI 獲取到的具體實現,例項化各個實現類。
-
根據第一步獲取到的驅動列表來例項化具體實現類。
我們重點關注 SPI 的用法,首先看第二步,使用 SPI 來獲取驅動的實現類 , 對應的程式碼是:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
這裡沒有去 META-INF/services
目錄下查詢配置檔案,也沒有載入具體實現類,做的事情就是封裝了我們的介面型別和類載入器,並初始化了一個迭代器。
接著看第三步,遍歷使用SPI獲取到的具體實現,例項化各個實現類,對應的程式碼如下:
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍歷所有的驅動實現
while(driversIterator.hasNext()) {
driversIterator.next();
}
在遍歷的時候,首先呼叫driversIterator.hasNext()
方法,這裡會搜尋 classpath 下以及 jar 包中所有的META-INF/services
目錄下的java.sql.Driver
檔案,並找到檔案中的實現類的名字,此時並沒有例項化具體的實現類。
然後是呼叫driversIterator.next()
方法,此時就會根據驅動名字具體例項化各個實現類了,現在驅動就被找到並例項化了。
3 Java SPI 機制原始碼解析
我們根據第一節 JDK SPI 示例,學習 ServiceLoader
類的實現。
進入 ServiceLoader
類的load
方法:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service , ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
上面的程式碼,load
方法會透過傳遞的服務型別和類載入器 classLoader
建立一個 ServiceLoader 物件。
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
// 快取已經被例項化的服務提供者,按照例項化的順序儲存
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
私有構造器會建立懶迭代器 LazyIterator 物件 ,所謂懶迭代器,就是物件初始化時,僅僅是初始化,只有在真正呼叫迭代方法時,才執行載入邏輯。
示例程式碼中建立完 serviceLoader 之後,接著呼叫iterator()
方法:
Iterator<Robot> iterator = serviceLoader.iterator();
// 迭代方法實現
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
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();
}
};
}
迭代方法的實現本質是呼叫懶迭代器 lookupIterator 的 hasNext()
和 next()
方法。
1、hasNext() 方法
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
懶迭代器的hasNextService
方法首先會透過載入器透過檔案全名獲取配置物件 Enumeration<URL> configs
,然後呼叫解析parse
方法解析classpath
下的META-INF/services/
目錄裡以服務介面命名的檔案。
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
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;
}
當 hasNextService
方法返回 true , 我們可以呼叫迭代器的 next
方法 ,本質是呼叫懶載入器 lookupIterator 的 next()
方法:
2、next()
方法
Robot robot = iterator.next();
// 呼叫懶載入器 lookupIterator 的 `next()` 方法
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 subtype");
}
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
}
透過反射方法 Class.forName()
載入類物件,並用newInstance
方法將類例項化,並把例項化後的類快取到providers
物件中,(LinkedHashMap<String,S>
型別,然後返回例項物件。
4 Java SPI 機制的缺陷
透過上面的解析,可以發現,我們使用 JDK SPI 機制的缺陷 :
- 不能按需載入,需要遍歷所有的實現,並例項化,然後在迴圈中才能找到我們需要的實現。如果不想用某些實現類,或者某些類例項化很耗時,它也被載入並例項化了,這就造成了浪費。
- 獲取某個實現類的方式不夠靈活,只能透過 Iterator 形式獲取,不能根據某個引數來獲取對應的實現類。
- 多個併發多執行緒使用 ServiceLoader 類的例項是不安全的。
5 Spring SPI 機制
Spring SPI 沿用了 Java SPI 的設計思想,Spring 採用的是 spring.factories
方式實現 SPI 機制,可以在不修改 Spring 原始碼的前提下,提供 Spring 框架的擴充套件性。
1、建立 MyTestService 介面
public interface MyTestService {
void printMylife();
}
2、建立 MyTestService 介面實現類
- WorkTestService :
public class WorkTestService implements MyTestService {
public WorkTestService(){
System.out.println("WorkTestService");
}
public void printMylife() {
System.out.println("我的工作");
}
}
- FamilyTestService :
public class FamilyTestService implements MyTestService {
public FamilyTestService(){
System.out.println("FamilyTestService");
}
public void printMylife() {
System.out.println("我的家庭");
}
}
3、在資原始檔目錄,建立一個固定的檔案 META-INF/spring.factories
。
#key是介面的全限定名,value是介面的實現類
com.courage.platform.sms.demo.service.MyTestService = com.courage.platform.sms.demo.service.impl.FamilyTestService,com.courage.platform.sms.demo.service.impl.WorkTestService
4、執行程式碼
// 呼叫 SpringFactoriesLoader.loadFactories 方法載入 MyTestService 介面所有實現類的例項
List<MyTestService> myTestServices = SpringFactoriesLoader.loadFactories(
MyTestService.class,
Thread.currentThread().getContextClassLoader()
);
for (MyTestService testService : myTestServices) {
testService.printMylife();
}
執行結果:
FamilyTestService
WorkTestService
我的家庭
我的工作
Spring SPI 機制非常類似 ,但還是有一些差異:
- Java SPI 是一個服務提供介面對應一個配置檔案,配置檔案中存放當前介面的所有實現類,多個服務提供介面對應多個配置檔案,所有配置都在 services 目錄下。
- Spring SPI 是一個 spring.factories 配置檔案存放多個介面及對應的實現類,以介面全限定名作為key,實現類作為value來配置,多個實現類用逗號隔開,僅
spring.factories
一個配置檔案。
和 Java SPI 一樣,Spring SPI 也無法獲取某個固定的實現,只能按順序獲取所有實現。
6 Dubbo SPI 機制
基於 Java SPI 的缺陷無法支援按需載入介面實現類,Dubbo 並未使用 Java SPI,而是重新實現了一套功能更強的 SPI 機制。
Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,透過 ExtensionLoader,我們可以載入指定的實現類。
Dubbo SPI 所需的配置檔案需放置在 META-INF/dubbo
路徑下,配置內容如下:
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
與 Java SPI 實現類配置不同,Dubbo SPI 是透過鍵值對的方式進行配置,這樣我們可以按需載入指定的實現類。
另外,在測試 Dubbo SPI 時,需要在 Robot 介面上標註 @SPI 註解。
下面來演示 Dubbo SPI 的用法:
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<Robot> extensionLoader =
ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();
}
}
測試結果如下 :
另外,Dubbo SPI 除了支援按需載入介面實現類,還增加了 IOC 和 AOP 等特性 。
Dubbo SPI :
https://cn.dubbo.apache.org/zh-cn/docsv2.7/dev/source/dubbo-spi/
JDK/Dubbo/Spring 三種 SPI 機制,誰更好 ?
https://segmentfault.com/a/1190000039812642