參見:https://zhuanlan.zhihu.com/p/692383462
----- 部落格原文 -----
SPI機制介紹
前言
講 SPI 機制之前,先說說 API ,從面向介面程式設計的思想來看,「服務呼叫方」應該透過呼叫「介面」而不是「具體實現」來處理邏輯。那麼,對於「介面」的定義,應該在「服務呼叫方」還是「服務提供方」呢?
一般來說,會有兩種選擇:
- 「介面」定義在「服務提供方」
- 「介面」定義在「服務呼叫方」
情況1: 先來看看「介面」屬於「提供方」的情況。這個很容易理解,提供方同時提供了「介面」和「實現類」,「呼叫方」可以呼叫介面來達到呼叫某實現類的功能,這就是我們日常使用的 API 。 API 的顯著特徵就是:
介面和實現都在服務提供方中。自定義介面,自己去實現這個介面,也就是提供實現類,最後提供給外部去使用
情況2: 那麼再來看看「介面」屬於「呼叫方」的情況。這個其實就是 SPI 機制。以 JDBC 驅動為例,「呼叫方」定義了java.sql.Driver
介面(沒有實現這個介面),這個介面位於「呼叫方」JDK 的包中,各個資料庫廠商(也就是服務提供方)實現了這個介面,比如 MySQL 驅動 com.mysql.jdbc.Driver 。 SPI最顯著的特徵就是:
「介面」在「呼叫方」的包,「呼叫方」定義規則,而實現類在「服務提供方」中
總結一下:
- API 其實是服務提供方,它把介面和實現都做了,然後提供給服務呼叫方來用,服務提供方是處於主導地位的,此時服務呼叫方是被動的
- SPI 則是服務呼叫方去定義了某種標準(介面),你要給我提供服務,就必須按照我的這個標準來做實現,此時服務呼叫方的處於主導的,而服務提供方是被動的
概念
SPI 全稱:Service Provider Interface,是Java提供的一套用來被第三方實現或者擴充套件的介面,它可以用來啟用框架擴充套件和替換元件。
這是一種JDK內建的一種服務發現的機制,用於制定一些規範,實際實現方式交給不同的服務廠商。如下圖:
面向的物件的設計裡,我們一般推薦模組之間基於介面程式設計,模組之間不對實現類進行硬編碼。一旦程式碼裡涉及具體的實現類,就違反了可插拔的原則,如果需要替換一種實現,就需要修改程式碼。
Q:上圖的基於介面程式設計體現在哪裡?可插拔又是指什麼? A:呼叫方只會去依賴上圖的標準服務介面,而不會去管實現類,這樣做的優點有哪些呢? 1. 如果你想再增加一個實現類,你只需要去實現這個介面就可以,其他地方的程式碼都不用動,這是擴充套件性的體現 2. 又或者是某天你不需要實現類A了,那直接把實現類A去掉就可以了,對整個系統不會有大的改動,這就是可插拔和元件化思想的好處,此時整個系統還實現了充分的解偶
SPI 應用案例之 JDBC DriverManager
眾所周知,關係型資料庫有很多種,如:MySQL、Oracle、PostgreSQL 等等。Java 的 JDBC 提供了一套 API 供 Java 應用與資料庫進行互動,但是,不同的資料庫在底層實現上是有區別的呀,我現在想用這一套 API 對所有資料庫都適用,那怎麼辦勒?此時就出現了一個東西,這個東西就是驅動。
舉個例子:這就相比於你說中文,但是你的客戶可能有說英語、法語、德語等等,此時你是不是希望有個翻譯,而且是有多個翻譯,有翻譯成英語的,翻譯法語的等等(假設一個翻譯只能把中文翻譯成一種語言),有了不同的翻譯之後,這樣就可以把你說的中文翻譯給不同語言的人聽,而驅動就是翻譯
實現 SPI 的四步:
- 服務的呼叫者要先定義好介面
- 服務的提供者提供了介面的實現
- 需要在類目錄下的 META-INF/services/ 目錄裡建立一個以服務介面命名的檔案,這個檔案裡的內容就是這個介面的具體的實現類。
- 當其他的程式需要這個服務(服務提供者提供的)的時候,就可以透過查詢這個jar包(一般都是以jar包做依賴)的META-INF/services/中的配置檔案,配置檔案中有介面的具體實現類名,可以根據這個類名進行載入例項化,就可以使用該服務了。
接下來看看 JDBC 的實踐是怎麼做的:
- 這是 java.sql.Driver(JDK)中定義驅動的介面(對應在服務的呼叫者要先定義好介面)
1.這是MySQL驅動中的Driver類,它實現了上面的Driver介面
1.並且我們發現在META-INF/services/ 目錄裡建立一個以服務介面(java.sql.Driver)命名的檔案,這個檔案裡的內容就是這個介面的具體的實現類
1.怎麼去把驅動的服務提供給呼叫者呢?現在常用的就是直接引入依賴就可以
SPI 原理
上文中,我們瞭解了使用 Java SPI 的方法。那麼 Java SPI 是如何工作的呢?實際上,Java SPI 機制依賴於 ServiceLoader 類去解析、載入服務。因此,掌握了 ServiceLoader 的工作流程,就掌握了 SPI 的原理。ServiceLoader 的程式碼本身很精練,接下來,讓我們透過讀原始碼的方式,逐一理解 ServiceLoader 的工作流程。
ServiceLoader 的成員變數
先看一下 ServiceLoader 類的成員變數,大致有個印象,後面的原始碼中都會使用到。
public final class ServiceLoader<S> implements Iterable<S> {
// SPI 配置檔案目錄
private static final String PREFIX = "META-INF/services/";
// 將要被載入的 SPI 服務
private final Class<S> service;
// 用於載入 SPI 服務的類載入器
private final ClassLoader loader;
// ServiceLoader 建立時的訪問控制上下文
private final AccessControlContext acc;
// SPI 服務快取,按例項化的順序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懶查詢迭代器
private LazyIterator lookupIterator;
// ...
}
ServiceLoader 的工作流程
(1)ServiceLoader.load 靜態方法
應用程式載入 Java SPI 服務,都是先呼叫 ServiceLoader.load 靜態方法,這個方法會new ServiceLoader物件 ServiceLoader.load 靜態方法的作用是: ① 指定類載入 ClassLoader 和訪問控制上下文; ② 然後,重新載入 SPI 服務
- 清空快取中所有已例項化的 SPI 服務
- 根據 ClassLoader 和 SPI 型別,建立懶載入迭代器
這裡,摘錄 ServiceLoader.load 相關原始碼,如下:
// service 傳入的是期望載入的 SPI 介面型別
// loader 是用於載入 SPI 服務的類載入器
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
public void reload() {
// 清空快取中所有已例項化的 SPI 服務
providers.clear();
// 根據 ClassLoader 和 SPI 型別,建立懶載入迭代器
lookupIterator = new LazyIterator(service, loader);
}
// 私有構造方法
// 重新載入 SPI 服務
private ServiceLoader(Class<S> 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 服務
reload();
}
(2)應用程式透過 ServiceLoader 的 iterator 方法遍歷 SPI 例項 ServiceLoader 的類定義,明確了 ServiceLoader 類實現了 Iterable 介面,所以,它是可以迭代遍歷的。實際上,ServiceLoader 類維護了一個快取 providers( LinkedHashMap 物件),快取 providers 中儲存了已經被成功載入的 SPI 例項,這個 Map 的 key 是 SPI 介面實現類的全限定名,value 是該實現類的一個例項物件。 當應用程式呼叫 ServiceLoader 的 iterator 方法時,ServiceLoader 會先判斷快取 providers 中是否有資料:如果有,則直接返回快取 providers 的迭代器;如果沒有,則返回懶載入迭代器的迭代器。
public Iterator<S> iterator() {
return new Iterator<S>() {
// 快取 SPI providers
Iterator<Map.Entry<String,S>> 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 關鍵程式碼 hasNextService 方法:
- 拼接 META-INF/services/ + SPI 介面全限定名
- 透過類載入器,嘗試載入資原始檔
- 解析資原始檔中的內容,獲取 SPI 介面的實現類的全限定名 nextName
nextService 方法:
- hasNextService () 方法解析出了 SPI 實現類的的全限定名 nextName,透過反射,獲取 SPI 實現類的類定義 Class。
- 然後,嘗試透過 Class 的 newInstance 方法例項化一個 SPI 服務物件。如果成功,則將這個物件加入到快取 providers 中並返回該物件。
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 1.拼接 META-INF/services/ + SPI 介面全限定名
// 2.透過類載入器,嘗試載入資原始檔
// 3.解析資原始檔中的內容
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"