SPI的全稱是(Service Provider Interface)是服務(wù)提供接口的意思。如果我們不寫框架性代碼或者開發(fā)插件的話,對(duì)于SPI機(jī)制可能不會(huì)那么熟悉,但如果我們閱讀諸如Dubbo、JDBC數(shù)據(jù)庫驅(qū)動(dòng)包、Spring以及最近比較流行的Spring Boot相關(guān)starter組件源碼的話,就會(huì)發(fā)現(xiàn)SPI機(jī)制及其思想在這些框架中有大量的應(yīng)用。
我們知道系統(tǒng)中抽象的各個(gè)模塊,往往會(huì)有很多不同的實(shí)現(xiàn)方案,例如我們常見的日志模塊方案、xml解析模塊、JDBC驅(qū)動(dòng)模塊等。在面向?qū)ο蟮脑O(shè)計(jì)思想中,我們一般推薦模塊之間的對(duì)接基于面向接口的編程方式,而不是直接面向?qū)崿F(xiàn)類硬編碼。因?yàn)?,一旦代碼里涉及具體的實(shí)現(xiàn)類的耦合,就違反了可插拔、閉開等原則,如果需要替換一種實(shí)現(xiàn)方式,就需要修改代碼。如果我們希望實(shí)現(xiàn)在模塊裝配的時(shí)候能夠不在程序硬編碼指定,那就需要一種服務(wù)發(fā)現(xiàn)的機(jī)制(PS:不要和現(xiàn)在微服務(wù)的服務(wù)發(fā)現(xiàn)機(jī)制搞混淆了)。

JAVA中的SPI技術(shù)就是提供了這樣一個(gè)為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)類的機(jī)制,這一點(diǎn)也類似于Spring框架中的IOC思想,就是將程序加載裝配的控制權(quán)移到程序之外,這個(gè)機(jī)制在組件模塊化設(shè)計(jì)中非常重要!那么在JAVA中SPI機(jī)制具體是如何約定的呢?
在JAVA SPI機(jī)制中約定,當(dāng)服務(wù)的提供者(例如某個(gè)新的日志組件),提供了服務(wù)接口的某種實(shí)現(xiàn)之后,在jar包的META-INF/services/目錄中同時(shí)創(chuàng)建一個(gè)以該服務(wù)接口命名的文件,文件中填寫了實(shí)現(xiàn)該服務(wù)接口具體實(shí)現(xiàn)類的全限定類名。這樣,在當(dāng)外部程序裝配這個(gè)模塊的時(shí)候,就可以通過該jar包中META-INF/services/目錄里的配置文件找到具體的實(shí)現(xiàn)類路徑,從而可以通過反射機(jī)制裝載實(shí)例化,從而完成模塊的注入。例如,我們Dubbo框架時(shí),除了引入核心依賴jar外,還會(huì)有很多擴(kuò)展組件如dubbo-monitor,如果我們需要引入此組件只需要簡單引入就可以,而不需要做額外的集成,主要原因就是因?yàn)樵摻M件時(shí)以SPI機(jī)制進(jìn)行的集成。
綜上所述,SPI機(jī)制實(shí)際上就是“基于接口的編程+策略模式+配置文件”組合實(shí)現(xiàn)的一種動(dòng)態(tài)加載機(jī)制,在JDK中提供了工具類:“java.util.ServiceLoader”來實(shí)現(xiàn)服務(wù)查找。
JDK SPI機(jī)制實(shí)現(xiàn)示例
JDK中自帶對(duì)SPI機(jī)制的支持,主要是涉及java.util.ServiceLoader類的使用,接下來,我們通過一個(gè)簡單的代碼示例來理解下JAVA中SPI機(jī)制的實(shí)現(xiàn)方式吧!我們先通過一張圖來看看使用JAVA SPI機(jī)制需要遵循什么規(guī)范吧:
└── src/main/java
├── cn
│ └── wudimanong
│ └── spi
│ ├── SPIService.java
│ ├── impl
│ │ ├── SpiImpl1.java
│ │ └── SpiImpl2.java
│ └── SPIMain.java
└── META-INF
└── services
└── com.wudimanong.spi.SPIService
1、我們需要在項(xiàng)目中創(chuàng)建目錄META-INF\services
2、定義一個(gè)接口服務(wù)提供,如SpiService
public interface SpiService {
void execute();
}
3、分別定義兩個(gè)服務(wù)接口實(shí)現(xiàn)類,如:SpiImpl1,SpiImpl2
public class SpiImpl1 implements SPIService {
@Override
public void execute() {
System.out.println("SpiImpl1 Hello.");
}
}
// ------------------------------------------
public class SpiImpl2 implements SPIService {
@Override
public void execute() {
System.out.println("SpiImpl2 Hello.");
}
}
4、我們?cè)?code>ClassPath路徑下添加一個(gè)配置文件,文件的名稱是接口的全限定類名,內(nèi)容則是實(shí)現(xiàn)類的全限定類名,如果是多個(gè)實(shí)現(xiàn)類則用換行符分割,文件路徑如下

文件內(nèi)容如下:
cn.wudimanong.spi.impl.SpiImpl1
cn.wudimanong.spi.impl.SpiImpl2
這樣,我們基本上就遵循JAVA SPI的機(jī)制定義了組件基本結(jié)構(gòu),最后我們通過編寫測(cè)試類,看看如果使用SPI機(jī)制,客戶端代碼應(yīng)該如何寫:
public class SPIMain {
public static void main(String[] args) {
ServiceLoader<SPIService> loaders =
ServiceLoader.load(SPIService.class);
Iterator<SPIService> spiServiceIterator = loaders.iterator();
System.out.println("classPath:" + System.getProperty("java.class.path"));
while (spiServiceIterator.hasNext()) {
SPIService spiService = spiServiceIterator.next();
System.out.println(spiService.execute());
}
}
}
可以看到在引入方加載組件模塊時(shí)是通過ServiceLoader這個(gè)類來操作的,如果我們打開該類的源碼,就可以看到,其主要實(shí)現(xiàn)邏輯其實(shí)就是在META-INF/services/目錄中查找實(shí)現(xiàn)類,并進(jìn)行相關(guān)實(shí)例化操作。關(guān)于該類的部分關(guān)鍵源代碼如下:
public final class ServiceLoader<S> implements Iterable<S>{
//配置文件的路徑
private static final String PREFIX = "META-INF/services/";
//加載的服務(wù)類或接口
private final Class<S> service;
//已加載的服務(wù)類集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//類加載器
private final ClassLoader loader;
//內(nèi)部類,真正加載服務(wù)類
private LazyIterator lookupIterator;
public void reload() {
//先清空
providers.clear();
//實(shí)例化內(nèi)部類
lookupIterator = new LazyIterator(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方法如上
reload();
}
}
而后面查找類和創(chuàng)建實(shí)現(xiàn)類的過程就都是在LazyIterator類完成的了,由于篇幅的關(guān)系,感興趣的朋友可以點(diǎn)開源碼自行閱讀一番!
JDBC數(shù)據(jù)庫驅(qū)動(dòng)包中SPI機(jī)制分析
通過上面的描述,相信大家對(duì)Java SPI機(jī)制的實(shí)現(xiàn)應(yīng)該是有了一個(gè)基本的認(rèn)識(shí),接下來我們以JDBC數(shù)據(jù)庫驅(qū)動(dòng)設(shè)計(jì)來看下Java SPI機(jī)制的真實(shí)應(yīng)用場(chǎng)景。我們知道通常各大數(shù)據(jù)庫廠商(如Mysql、Oracle)都會(huì)根據(jù)一個(gè)統(tǒng)一的規(guī)范,如:java.sql.Driver去開發(fā)各自的驅(qū)動(dòng)實(shí)現(xiàn)邏輯。
而我們?cè)谑褂玫膉dbc的時(shí)候客戶端卻是不需要改變代碼的,直接引入不同的SPI接口服務(wù)即可。例如以Mysql的JDBC驅(qū)動(dòng)jar來說:

這樣在引入mysql驅(qū)動(dòng)包后jdbc連接代碼java.sql.DriverManager,就會(huì)使用SPI機(jī)制來加載具體的jdbc實(shí)現(xiàn),關(guān)鍵源碼如下:
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
}
可以看到粉紅色代碼部分,JDBC驅(qū)動(dòng)管理代碼就是通過原生的Java SPI機(jī)制來實(shí)現(xiàn)加載具體的Mysql JDBC驅(qū)動(dòng)的。
Dubbo框架中SPI機(jī)制分析
需要說明的是雖然Java 提供了對(duì)SPI機(jī)制的默認(rèn)實(shí)現(xiàn)支持,但是并不表示所有的框架都會(huì)默認(rèn)使用這種Java自帶的邏輯,SPI機(jī)制更多的是一種實(shí)現(xiàn)思想,而具體的實(shí)現(xiàn)邏輯,則是可以自己定義的。例如我們說Dubbo框架中大量使用了SPI技術(shù),但是Dubbo并沒有使用JDK原生的ServiceLoader,而是自己實(shí)現(xiàn)了ExtensionLoader來加載擴(kuò)展點(diǎn),所以我們看Dubbo框架源碼的時(shí)候,千萬不要被配置目錄是/META-INF/dubbo/internal,而不是META-INF/services/所迷惑了。

相應(yīng)地如果其他框架中也使用了自定義的SPI機(jī)制實(shí)現(xiàn),也不要疑惑,它們也只是重新定義了類似于ServiceLoader類的加載邏輯而已,其背后的設(shè)計(jì)思想和原理則都是一樣的!例如,以Dubbo中卸載協(xié)議的代碼舉例:
private void destroyProtocols() {
ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
for (String protocolName : loader.getLoadedExtensions()) {
try {
Protocol protocol = loader.getLoadedExtension(protocolName);
if (protocol != null) {
protocol.destroy();
}
} catch (Throwable t) {
logger.warn(t.getMessage(), t);
}
}
}
而ExtensionLoader中掃描的配置路徑如下:
public class ExtensionLoader<T> {
private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);
private static final String SERVICES_DIRECTORY = "META-INF/services/";
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
}
以上通過對(duì)Java自帶SPI機(jī)制的示例以及對(duì)Dubbo和JDBC驅(qū)動(dòng)框架中對(duì)SPI機(jī)制應(yīng)用的分析,相信大家應(yīng)該是有了一個(gè)總體的原理性的認(rèn)識(shí)了。如果需要更加深入的了解一些細(xì)節(jié)的實(shí)現(xiàn)邏輯就需要大家好好去看看ServiceLoader的源碼了,如果其他框架單獨(dú)實(shí)現(xiàn)了SPI機(jī)制,其相應(yīng)的實(shí)現(xiàn)加載工具類也需要具體看看它們的源碼是怎么實(shí)現(xiàn)的了!