Java SPI 機制從原理到實戰
1. 什麼是 SPI
1. 背景
在面向物件的設計原則中,一般推薦模組之間基於介面程式設計,通常情況下呼叫方模組是不會感知到被呼叫方模組的內部具體實現。一旦程式碼裡面涉及具體實現類,就違反了開閉原則。如果需要替換一種實現,就需要修改程式碼。
為了實現在模組裝配的時候不用在程式裡面動態指明,這就需要一種服務發現機制。Java SPI 就是提供了這樣一個機制:為某個介面尋找服務實現的機制。這有點類似 IOC 的思想,將裝配的控制權移交到了程式之外。
SPI
英文為 Service Provider Interface
字面意思就是:“服務提供者的介面”,我的理解是:專門提供給服務提供者或者擴充套件框架功能的開發者去使用的一個介面。
SPI 將服務介面和具體的服務實現分離開來,將服務呼叫方和服務實現者解耦,能夠提升程式的擴充套件性、可維護性。修改或者替換服務實現並不需要修改呼叫方。
2. 使用場景
很多框架都使用了 Java 的 SPI 機制,比如:資料庫載入驅動,日誌介面,以及 dubbo 的擴充套件實現等等。
3. SPI 和 API 有啥區別
說到 SPI 就不得不說一下 API 了,從廣義上來說它們都屬於介面,而且很容易混淆。下面先用一張圖說明一下:
一般模組之間都是通過通過介面進行通訊,那我們在服務呼叫方和服務實現方(也稱服務提供者)之間引入一個“介面”。
當實現方提供了介面和實現,我們可以通過呼叫實現方的介面從而擁有實現方給我們提供的能力,這就是 API ,這種介面和實現都是放在實現方的。
當介面存在於呼叫方這邊時,就是 SPI ,由介面呼叫方確定介面規則,然後由不同的廠商去根絕這個規則對這個介面進行實現,從而提供服務,舉個通俗易懂的例子:公司 H 是一家科技公司,新設計了一款晶片,然後現在需要量產了,而市面上有好幾家晶片製造業公司,這個時候,只要 H 公司指定好了這晶片生產的標準(定義好了介面標準),那麼這些合作的晶片公司(服務提供者)就按照標準交付自家特色的晶片(提供不同方案的實現,但是給出來的結果是一樣的)。
2. 實戰演示
Spring 框架提供的日誌服務 SLF4J 其實只是一個日誌門面(介面),但是 SLF4J 的具體實現可以有幾種,比如:Logback、Log4j、Log4j2 等等,而且還可以切換,在切換日誌具體實現的時候我們是不需要更改專案程式碼的,只需要在 Maven 依賴裡面修改一些 pom 依賴就好了。
這就是依賴 SPI 機制實現的,那我們接下來就實現一個簡易版本的日誌框架。
1. Service Provider Interface
新建一個 Java 專案 service-provider-interface
目錄結構如下:
├─.idea ├─lib │ └─service-provider-interface.jar └─src ├─META-INF │ └─services │ └─org.spi.service.Logger └─org └─spi └─provider └─Logback.java
新建 Logger 介面,這個就是 SPI , 服務提供者介面,後面的服務提供者就要針對這個介面進行實現。
package org.spi.service;
public interface Logger {
void info(String msg);
void debug(String msg);
}
接下來就是 LoggerService 類,這個主要是為服務使用者(呼叫方)提供特定功能的。如果存在疑惑的話可以先往後面繼續看。
package org.spi.service;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
public class LoggerService {
private static final LoggerService SERVICE = new LoggerService();
private final Logger logger;
private final List<Logger> loggerList;
private LoggerService() {
ServiceLoader<Logger> loader = ServiceLoader.load(Logger.class);
List<Logger> list = new ArrayList<>();
for (Logger log : loader) {
list.add(log);
}
// LoggerList 是所有 ServiceProvider
loggerList = list;
if (!list.isEmpty()) {
// Logger 只取一個
logger = list.get(0);
} else {
logger = null;
}
}
public static LoggerService getService() {
return SERVICE;
}
public void info(String msg) {
if (logger == null) {
System.out.println("info 中沒有發現 Logger 服務提供者");
} else {
logger.info(msg);
}
}
public void debug(String msg) {
if (loggerList.isEmpty()) {
System.out.println("debug 中沒有發現 Logger 服務提供者");
}
loggerList.forEach(log -> log.debug(msg));
}
}
新建 Main 類(服務使用者,呼叫方),啟動程式檢視結果。
package org.spi.service;
public class Main {
public static void main(String[] args) {
LoggerService service = LoggerService.getService();
service.info("Hello SPI");
service.debug("Hello SPI");
}
}
程式結果:
info 中沒有發現 Logger 服務提供者
debug 中沒有發現 Logger 服務提供者
將整個程式直接打包成 jar 包,可以直接通過 IDEA 將專案打包成一個 jar 包。
2. Service Provider
接下來新建一個專案用來實現 Logger 介面
新建專案 service-provider
目錄結構如下:
├─.idea ├─lib │ └─service-provider-interface.jar └─src ├─META-INF │ └─services │ └─org.spi.service.Logger └─org └─spi └─provider └─Logback.java
新建 Logback 類
package org.spi.provider;
import org.spi.service.Logger;
public class Logback implements Logger {
@Override
public void info(String msg) {
System.out.println("Logback info 的輸出:" + msg);
}
@Override
public void debug(String msg) {
System.out.println("Logback debug 的輸出:" + msg);
}
}
將 service-provider-interface
的 jar 匯入專案中。新建 lib 目錄,然後將 jar 包拷貝過來,再新增到專案中。
再點選 OK 。
接下來java培訓學習就可以在專案中匯入 jar 包裡面的一些類和方法了,就像 JDK 工具類導包一樣的。
實現 Logger 介面,在 src 目錄下新建 META-INF/services
資料夾,然後新建檔案 org.spi.service.Logger
(SPI 的全類名),檔案裡面的內容是:org.spi.provider.Logback
(Logback 的全類名,即 SPI 的實現類的包名 + 類名)。
這是 JDK SPI 機制 ServiceLoader 約定好的標準
接下來同樣將 service-provider
專案打包成 jar 包,這個 jar 包就是服務提供方的實現。通常我們匯入 maven 的 pom 依賴就有點類似這種,只不過我們現在沒有將這個 jar 包釋出到 maven 公共倉庫中,所以在需要使用的地方只能手動的新增到專案中。
3. 效果展示
接下來再回到 service-provider-interface
專案。
匯入 service-provider
jar 包,重新執行 Main 方法。執行結果如下:
Logback info 的輸出:Hello SPI
Logback debug 的輸出:Hello SPI
說明匯入 jar 包中的實現類生效了。
通過使用 SPI 機制,可以看出 服務(LoggerService)和 服務提供者兩者之間的耦合度非常低,如果需要替換一種實現(將 Logback 換成另外一種實現),只需要換一個 jar 包即可。這不就是 SLF4J 原理嗎?
如果某一天需求變更了,此時需要將日誌輸出到訊息佇列,或者做一些別的操作,這個時候完全不需要更改 Logback 的實現,只需要新增一個 服務實現(service-provider)可以通過在本專案裡面新增實現也可以從外部引入新的服務實現 jar 包。我們可以在服務(LoggerService)中選擇一個具體的 服務實現
(service-provider) 來完成我們需要的操作。
loggerList.forEach(log -> log.debug(msg));
或者
loggerList.get(1).debug(msg);
loggerList.get(2).debug(msg);
這裡需要先理解一點:ServiceLoader 在載入具體的 服務實現 的時候會去掃描所有包下 src 目錄的 META-INF/services
的內容,然後通過反射去生成對應的物件,儲存在一個 list 列表裡面,所以可以通過迭代或者遍歷的方式得到你需要的那個 服務實現。
3. ServiceLoader
想要使用 Java 的 SPI 機制是需要依賴 ServiceLoader 來實現的,那麼我們接下來看看 ServiceLoader 具體是怎麼做的:
ServiceLoader 是 JDK 提供的一個工具類, 位於package java.util;
包下。
A facility to load implementations of a service.
這是 JDK 官方給的註釋:一種載入服務實現的工具。
再往下看,我們發現這個類是一個 final 型別的,所以是不可被繼承修改,同時它實現了 Iterable 介面。之所以實現了迭代器,是為了方便後續我們能夠通過迭代的方式得到對應的服務實現。
public final class ServiceLoader<S> implements Iterable<S>{ xxx...}
可以看到一個熟悉的常量定義:
private static final String PREFIX = "META-INF/services/";
下面是 load 方法:可以發現 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);
}
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();
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
根據程式碼的呼叫順序,在 reload() 方法中是通過一個內部類 LazyIterator 實現的。先繼續往下面看。
ServiceLoader 實現了 Iterable 介面的方法後,具有了迭代的能力,在這個 iterator 方法被呼叫時,首先會在 ServiceLoader 的 Provider 快取中進行查詢,如果快取中沒有命中那麼則在 LazyIterator 中進行查詢。
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(); // 呼叫 LazyIterator
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next(); // 呼叫 LazyIterator
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
在呼叫 LazyIterator 時,具體實現如下:
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);
}
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//通過PREFIX(META-INF/services/)和類名 獲取對應的配置檔案,得到具體的實現類
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;
}
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);
}
}
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
4. 總結
其實不難發現,SPI 機制的具體實現本質上還是通過反射完成的。即:我們按照規定將要暴露對外使用的具體實現類在 META-INF/services/
檔案下宣告。
其實 SPI 機制在很多框架中都有應用:Spring 框架的基本原理也是類似的反射。還有 dubbo 框架提供同樣的 SPI 擴充套件機制。
通過 SPI 機制能夠大大地提高介面設計的靈活性,但是 SPI 機制也存在一些缺點,比如:
-
遍歷載入所有的實現類,這樣效率還是相對較低的;
-
當多個 ServiceLoader 同時 load 時,會有併發問題。