背景
一位前輩在一次技術(shù)分享中指出我們目前的包管理不規(guī)范,模塊間職責(zé)有重疊,理解成本高不易維護(hù),提出在開發(fā)過程中應(yīng)當(dāng)明確按照職責(zé)將服務(wù)劃分到對(duì)應(yīng)的模塊中。
比如我們把所有服務(wù)都放在service層,但其實(shí)服務(wù)也是分為基礎(chǔ)服務(wù)和業(yè)務(wù)邏輯服務(wù)的,或許把類似業(yè)務(wù)數(shù)據(jù)查詢組裝服務(wù)放在service層,把具體業(yè)務(wù)邏輯服務(wù)統(tǒng)一放在business層會(huì)更好,更利于基礎(chǔ)服務(wù)的復(fù)用。
但當(dāng)服務(wù)拆離到不同模塊進(jìn)行復(fù)用時(shí),可能在開發(fā)過程中出現(xiàn)服務(wù)依賴的問題,這部分依賴問題的解耦可以用到Java的SPI機(jī)制。顯然,我從來沒有聽說過SPI是什么,也不明白這有什么好處。
SPI是什么
翻遍各種網(wǎng)上資料,來來回回都是車轱轆話,互相抄來抄去講得并不通俗易懂,這里就用我自己的理解來解釋。
SPI(Service Provider Interface),大意是“服務(wù)提供者接口”,是指在服務(wù)使用方角度提出的“接口要求”,是對(duì)“服務(wù)提供方”提出的約定,簡(jiǎn)單說就是:“我需要這樣的服務(wù),現(xiàn)在你們來滿足”。
API(Application Programming Interface)與之相對(duì),是站在服務(wù)提供方角度提供的無需了解底層細(xì)節(jié)的操作入口,即“我有這樣的服務(wù)可以給你使用”。
SPI與API的出發(fā)點(diǎn)截然不同,但作用與目的是相同的,即面向接口編程,也就是解耦。同時(shí)SPI使用的是一種“插件思維”,即服務(wù)提供者負(fù)責(zé)所有的使用維護(hù),當(dāng)替換服務(wù)提供方時(shí)不要說調(diào)用方不修改代碼,連配置文件都不需要修改(不過可能要修改依賴的jar)。

為什么要用SPI
- 在某些情況下,我們無法預(yù)知將會(huì)使用哪一個(gè)服務(wù),比如無比經(jīng)典的JDBC驅(qū)動(dòng)、日志輸出;
- 某些情況下,服務(wù)提供方發(fā)生變化時(shí)服務(wù)調(diào)用方修改/維護(hù)代碼或配置的成本非常高,如Dubbo、Motan、Spring等框架實(shí)現(xiàn)擴(kuò)展。
舉個(gè)例子,隔壁部門覺得我們的一個(gè)現(xiàn)有服務(wù)很棒,希望我們?cè)谄鋵S铆h(huán)境部署一份,同時(shí)希望以后的所有迭代能夠給他們也更新。但是使用的自研中間件我們使用的內(nèi)網(wǎng)版本他們使用公網(wǎng)版本,支付上我們對(duì)接支付寶他們對(duì)接微信......在業(yè)務(wù)邏輯不變但切換基礎(chǔ)服務(wù)時(shí)應(yīng)該如何維護(hù)使成本最???
| 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) |
|---|---|---|
| 維護(hù)兩套代碼 | 邏輯一致 | 實(shí)現(xiàn)簡(jiǎn)單但維護(hù)成本高 |
| 同一套代碼,在業(yè)務(wù)邏輯中區(qū)分環(huán)境 | 維護(hù)成本低,統(tǒng)一管理 | 邏輯復(fù)雜,需要硬編碼,當(dāng)再出現(xiàn)新環(huán)境時(shí)還得折騰 |
| SPI“插件”方式 | 維護(hù)成本低,無需針對(duì)實(shí)現(xiàn)方硬編碼,更多新環(huán)境或服務(wù)提供方變化時(shí)修改簡(jiǎn)單且不影響原有邏輯 | 理解成本提高 |
這也許就是一些框架在發(fā)展過程中經(jīng)歷過的階段,可以發(fā)現(xiàn)使用“插件”能更好滿足這個(gè)需求。
SPI原理
試想一下,如果要實(shí)現(xiàn)這樣的解耦方式,理想情況下應(yīng)該如何做?不外乎就是以下幾點(diǎn):
- 服務(wù)調(diào)用方定義接口,并在主干服務(wù)中設(shè)置接入點(diǎn)
- 服務(wù)提供方實(shí)現(xiàn)接口,并按照約定將實(shí)現(xiàn)類放在調(diào)用方可達(dá)的位置
- 調(diào)用方基于約定找到對(duì)應(yīng)位置,將對(duì)應(yīng)接口的實(shí)現(xiàn)類加載到內(nèi)存并連接至接入點(diǎn)
- 后續(xù)服務(wù)提供方發(fā)生變更/替換時(shí),只要仍然保持按照約定將新的提供方實(shí)現(xiàn)類替換到對(duì)應(yīng)位置即可,調(diào)用方無需任何修改
這是一種與IOC相同的思路,將裝配控制權(quán)轉(zhuǎn)移至程序外,由配置決定,切換成本低。
java.util.ServiceLoader提供的SPI加載方式
這個(gè)類非常簡(jiǎn)單,是原生支持的SPI加載方式,實(shí)際代碼量也就200行左右。
關(guān)鍵點(diǎn):
- 關(guān)鍵方法簽名:
public static <S> ServiceLoader<S> load(Class<S> service)- 實(shí)現(xiàn)了前文中的第1點(diǎn),即提供接入點(diǎn)設(shè)置
- 在服務(wù)的接入中,形如
ServiceLoader<SomeService> loader = ServiceLoader.load(SomeService.class);可設(shè)置接入對(duì)應(yīng)的接口
- 常量:
private static final String PREFIX = "META-INF/services/";- 約定了上述第2點(diǎn)中指定的位置,基于約定的配置讀取會(huì)從這里查找,當(dāng)然這是指服務(wù)提供方提供的jar中的
META-INF/services/目錄
- 約定了上述第2點(diǎn)中指定的位置,基于約定的配置讀取會(huì)從這里查找,當(dāng)然這是指服務(wù)提供方提供的jar中的
- 服務(wù)提供方的實(shí)現(xiàn)類在jar中,而只要在提供方定義好實(shí)現(xiàn)類與調(diào)用方接口之間的關(guān)系即可滿足調(diào)用方的加載需求
- 實(shí)現(xiàn)了上述第4點(diǎn)中的,只需要提供方按照約定提供實(shí)現(xiàn)類及實(shí)現(xiàn)關(guān)系,可以做到提供方替換時(shí)調(diào)用方無需任何修改
- 在對(duì)應(yīng)位置
META-INF/services/下,文件名應(yīng)為接口全限定名,內(nèi)容每行為一個(gè)實(shí)現(xiàn)類全限定名
- 類簽名:
public final class ServiceLoader<S> implements Iterable<S>- ServiceLoader實(shí)現(xiàn)了Iterable接口,因?yàn)閷?shí)現(xiàn)類與接口之間是多對(duì)一關(guān)系,服務(wù)提供方是有可能對(duì)一個(gè)接口提供多種實(shí)現(xiàn)的,因此加載時(shí)也可以加載多個(gè)實(shí)現(xiàn)類
- 迭代器簽名:
private class LazyIterator implements Iterator<S>,實(shí)現(xiàn)了懶加載迭代,即迭代到對(duì)應(yīng)的類才加載對(duì)應(yīng)的類
- 迭代器中的方法:
private boolean hasNextService()和private S nextService()- 分別對(duì)應(yīng)了迭代器中的
hasNext()方法和next()方法 - 實(shí)現(xiàn)了前文中第3點(diǎn),即從約定位置讀取實(shí)現(xiàn)類的全限定名稱,并從jar中加載對(duì)應(yīng)的類
- 使用
Class.forName加載類,使用newInstance初始化實(shí)例,cast進(jìn)行強(qiáng)制類型轉(zhuǎn)換最終得到實(shí)例,因此實(shí)現(xiàn)類必須提供無參構(gòu)造方法
- 分別對(duì)應(yīng)了迭代器中的
怎樣使用SPI
清楚原理后,使用方式就很好理解。
step.1 調(diào)用方定義接口
package com.xxx;
public interface IHelloWorld {
void sayHello();
}
step.? 使用API方式實(shí)現(xiàn)接口
非必選,對(duì)照看一下非SPI的方式。
package com.xxx;
public class HelloWorldApi implements IHelloWorld {
@Override
public void sayHello() {
System.out.println("Hello API!");
}
}
step.2 調(diào)用方在業(yè)務(wù)代碼中使用ServiceLoader
package com.xxx;
import java.util.ServiceLoader;
public class Main {
public static void main(String[] args) {
// 使用API
IHelloWorld helloWorldApi = new HelloWorldApi();
helloWorldApi.sayHello();
// 使用SPI
ServiceLoader<IHelloWorld> loader = ServiceLoader.load(IHelloWorld.class);
for (IHelloWorld helloWorldSpi : loader) {
helloWorldSpi.sayHello();
}
}
}
主要區(qū)別在于SPI方式并不需要知道實(shí)現(xiàn)類是誰,完全面向接口使用,類似RPC調(diào)用的情況;而API要求在業(yè)務(wù)方代碼/配置中指明實(shí)現(xiàn)類。
step.3 提供方實(shí)現(xiàn)接口
這里提供兩個(gè)實(shí)現(xiàn)類。
package com.xxx;
public class HelloWorldSpi1 implements IHelloWorld {
@Override
public void sayHello() {
System.out.println("Hello SPI 1!");
}
}
package com.xxx;
public class HelloWorldSpi2 implements IHelloWorld {
@Override
public void sayHello() {
System.out.println("Hello SPI 2!");
}
}
可以看出,實(shí)現(xiàn)方式與API方式完全一致。
step.4 提供方提供配置
文件位于/resources/META-INF/services,文件名為com.xxx.IHelloWorld即接口全限定名稱。
/resources/META-INF/services/com.xxx.IHelloWorld的內(nèi)容為兩個(gè)實(shí)現(xiàn)類的全限定名稱:
com.xxx.HelloWorldSpi1
com.xxx.HelloWorldSpi2
ps. 通常調(diào)用方與提供方不在同一個(gè)jar中
輸出結(jié)果
Hello API!
Hello SPI 1!
Hello SPI 2!
具體應(yīng)用方式
參考我們常用的JDBC,我們?cè)谕惶状a中可能需要利用相同接口但不同實(shí)現(xiàn)的情況下,可以在代碼中利用SPI接入面向接口編程,在業(yè)務(wù)中不考慮具體的底層實(shí)現(xiàn)。
具體的底層實(shí)現(xiàn)可以分離出來,將每組實(shí)現(xiàn)和SPI配置文件打包成不同的jar,在具體使用時(shí)根據(jù)需要使用不同的jar即可。

在mysql-connector-java:5.1.47包的META-INF/services/目錄下有個(gè)java.sql.Driver文件,內(nèi)容為:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
這是JDBC 4.0之后使用SPI機(jī)制直接獲取實(shí)現(xiàn),避免之前使用Class.forName("com.mysql.jdbc.Driver")方式加載MySQL驅(qū)動(dòng)時(shí)的硬編碼。詳情可見java.sql.DriverManager類中的靜態(tài)代碼塊:
static {
loadInitialDrivers(); // 這里使用ServiceLoader獲取具體的Driver接口實(shí)現(xiàn)
println("JDBC DriverManager initialized");
}
原生SPI的缺點(diǎn)
- 只能根據(jù)提供方的配置來獲取實(shí)現(xiàn)類,當(dāng)提供方提供多個(gè)實(shí)現(xiàn)時(shí)無法直接指定具體使用哪一個(gè)實(shí)現(xiàn)。當(dāng)然,這正是這個(gè)解耦機(jī)制上必須要做的犧牲,否則就破壞了“不修改代碼”的初衷。但是這一點(diǎn)可以在自定義擴(kuò)展時(shí)優(yōu)化
- 非單例,每次load都會(huì)創(chuàng)建新的實(shí)例,建議自行優(yōu)化,注意并發(fā)問題