錦囊篇|Java中的SPI機制

一塊兒用Gradle Transform API + ASM完成代碼織入呀~[1]這篇文章中我曾經說起關於SPI的方案,這篇文章針對的內容有三點:爲何當初要選擇SPI,他的實現流程是什麼樣的,以及它存在什麼樣的問題。web

什麼是SPI

Service Provider Interface翻譯成中文就是服務提供接口,簡稱SPI,它是JDK內置的一種機制,用途就是本地服務發現和提供。編程

用一個簡單的案例來講明上面的圖:設計模式

今天是星期六沒得上班,也就意味着小易同窗得在家裏把吃飯(調用方) 的問題解決了,那這個時候小易瘋狂轉動大腦想該吃啥(標準服務接口),擺在小易面前有兩個選擇:外賣、樓下的飯店(服務提供方)。最後小易同窗選擇吃了樓下便宜又方便的大排面,畢竟貧窮限制了選擇空間。api

Java中經過基於接口的編程+策略模式+配置文件來實現SPI這一套機制。緩存

另外這裏須要說起的內容有一點就是設計模式之六大原則中的接口隔離,通常咱們是不會在一個接口類型中定義過多的方法,這也是爲了保障更改後最小化的影響。安全

關於接口隔離等設計模式的內容,詳見於設計模式的十八般武藝[2]微信

如何使用

// 1
public interface Robot {
void sayHello();
}
// 2
public class OptimusPrime implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Optimus Prime.");
}
}
// 3
public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}

先定義上述的三個類,23分別接入了接口1並完成具體的實現。而後你要在資源目錄下建立一個文件,固然他存在固定的建立方式,建立META-INF/services/文件,文件名一樣存在建立的要求包名.接口類名若是你的module沒有接入它時,其實會出現這樣的結果。而若是存在時,你就能夠加入你的實現,而這個文件中加入的類將成爲以後去用於發現服務的基礎。網絡

這裏咱們使用系統提供的ServiceLoader來完成服務的發現,ServiceLoader<Robot> services = ServiceLoader.load(Robot.class);。使用ServiceLoader.iterator()能夠直接用迭代器的方式來完成數據的遍歷,若是出現了上述具體類中的打印數聽說明獲取成功了。多線程

源碼分析

整個ServiceLoader的代碼數量級其實也才587行。仍是之前的分析法,咱們從調用點開始app

public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader(); //若是咱們不提供類加載器,他會提供,一樣的這個加載器你能夠進行自定義
return ServiceLoader.load(service, cl); // 1 -->
}

// 1-->
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(); // 2 -->
}

// 2 -->
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}

一整條主線下來其實就是初始化了幾個變量,因此這裏須要先清楚如下須要實例化都有誰。

1. Class<S> service; // 那些會被加載進來的類或接口
2. ClassLoader loader; // 用於定位,加載和實例化providers的類加載器,以後會在建立service時使用
3. AccessControlContext acc; // 建立ServiceLoader時採用的訪問控制上下文
4. LinkedHashMap<String,S> providers // 緩存providers,按實例化的順序排列
5. LazyIterator lookupIterator; // 懶查找迭代器

讀到這裏是否是會有必定的質疑,既然有文件寫入,必定會有一個文件讀出的位置。可是從整條脈絡下來根本沒有任何跟文件讀取相關的內容,可是根據網上各類的用法來講這樣確定是沒有問題,可是爲何就沒找到呢?

private static final String PREFIX = "META-INF/services/";

這裏咱們只好用他已經定義好的靜態變量來進行尋找了,

private class LazyIterator implements Iterator<S> {
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;
}
}

經過尋找咱們可以發現他被藏在了一個叫作LazyIterator類的hashNextService()方法中,這裏須要先作一下回憶,是否是在那裏見過這個LazyIterator,若是沒有印象的讀者往前翻到reload()方法中,裏面有這樣的一段代碼lookupIterator = new LazyIterator(service, loader);也就是說其實最後的內容會被保存在這個變量中,幾個重要的目標點找到了,那咱們就找一下這個方法的整個調用鏈是什麼樣的。

hasNextService() <-- hasNext() <-- ServiceLoader.hasNext()
|-- nextService() <-- next() <-- ServiceLoader.next()

因此其實你在調用ServiceLodaer自定義的迭代器的時候就能夠去發現藏在項目的各類服務。可是爲何咱們都會說ServiceLodaer是一個消耗性能的方案呢,咱們用一段代碼做證。

Class.forName(cn, false, loader);

這段代碼存在於調用鏈nextService()中,而這個方法想來讀者都比較熟悉了,就是反射了,就是它的一個消耗性能可是百試不爽的方案。

場景分析

這裏將拿我以前的遇見的狀況來作一個分析,這裏須要作一個道歉,我這幾天想這個主題最後發現實際上是我當時想差了的問題,實際上是能夠不存在數據前置獲取的問題的。當時的代碼狀況是這樣的:

下面用一串僞代碼表示,由於當時使用是公司內部封裝過的ServiceLoader,若是找不到方法時會多作一層兜底,用代理生成一個對象。

main {
if (X) { A = B }
if (A) { do() }
}

ok~,大概就是上述的狀況,兩個存在相關性的判斷語句,與真實狀況大體相仿。這個X的數據來源源於咱們的網絡,可是我做爲線下工具,我但願可以得到一個全量的數據,那我勢必是須要突破這個判斷的。

那這個時候我就想說有兜底,ok!那我就用這個代理兜底來加入這一層的判斷,那咱們不是ok了?代碼就變成了這樣。

main {
if (X or ServiceLoader.load(M).class != Proxy.class) { A = B }
if (A) { do() }
}

只要加入這樣的一句話,咱們彷佛就能夠很是輕鬆的闖過前置的判斷,do()這個函數也能很是順利的執行,這樣看來這個方案惟一最大的缺點就是性能損耗了。那若是我說個人代碼裏面有十來須要這樣的去突破的口子的呢?你還會以爲這是一個好的方案嗎?

另外這裏還須要從幾個維度出發考慮:

  1. 改動成本,既然是要一個api層,那確定要有impl層,而上述這個須要兜底的庫確定也要接入api,那若是將來個人api改變,那兜底的庫和impl層都同時須要改變,改動成本相對較大。

  2. 控制粒度 / 現存的服務數量控制,若是後來有人接手了這個項目,他一樣繼承了個人這個接口,而後註冊爲成爲了一個服務,而剛好這個服務是要做爲線上服務存在的,可是他不知道我在這裏會被使用來進行判斷,那最後致使的結果就是上線之後,被個人這個判斷影響,致使本應該存量上報的數據,最後全量上報,導致流量劇烈波動最後致使的結果是小,可是對於用戶體驗而言咱們那裏面帶有的操做可能會出現一個全局性的影響。

經過以上幾點分析之後,咱們最後才把方案選擇爲了插樁。可是SPI他真的毫無用處嗎?結合咱們上述存在的問題,先從改動成本說,若是api是一個基本能夠說一塵不變的接口,那實現他的服務其實很天然的就可以避免這件事情,而咱們的注意力就能夠很天然的聚焦在實現上。 那通常什麼樣的場景下咱們纔會使用SPI機制呢?(這裏傾向我的理解)

  • 解耦
  • 根據實際需求 diy 接入實現

DubboJDBC等庫都是對SPI機制的最佳實踐。

總結

缺點:

  1. 這來自於ServiceLoader自己,即使使用了懶加載,但仍是遍歷獲取,最差的結果就是致使全部的具體實現所有被實例化一次。但這樣的狀況對於可能只須要特定的你而言是一種資源浪費,若是接入了過多的實現,那這個問題就會被無限放大。

  2. 多線程使用ServiceLoader類的線程不安全問題。

參考文檔

  1. 高級開發必須理解的Java中SPI機制:https://www.jianshu.com/p/46b42f7f593c

參考資料

[1]

一塊兒用Gradle Transform API + ASM完成代碼織入呀~: https://juejin.im/post/6863276629029126158

[2]

設計模式的十八般武藝: https://juejin.im/post/6844904147729252365#heading-6


本文分享自微信公衆號 - 告物(ClericYi_Android)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索