SPI機制是Dubbo框架的基礎知識,學習Dubbo框架之前有必要深入理解SPI機制。下面對SPI的概念,作用,使用以及原理作一個深入的介紹。
1.SPI是什麼?SPI有什麼用?
SPI的全稱是Service Provider Interface,可以翻譯爲“提供服務接口”;舉一個實際場景:模塊A提供一個Service接口,模塊B和模塊C分別實現該Service接口,那麼模塊A如何在無感知的情況下找到模塊B和模塊C中Service接口的實現類? SPI機制能解決這個問題。
很明顯,如果沒有SPI機制,那麼模塊B和模塊C對Service接口的實現類必定需要硬編碼在模塊A中,這就不符合“開閉原則”。所以SPI機制可以幫助模塊A自動尋找Service接口在其他jar包中的實現,簡單來說就是尋找接口的服務實現。
2.SPI如何使用?
下面具體用代碼演示SPI機制。
第一步:新建Maven項目ModelA。
ModelA項目只提供服務接口SPIService,沒有實現。
package com.starry.service;
public interface SPIService {
String spiService();
}
同時通過maven install
命令打包並上傳至本地倉庫,便於其他模塊依賴。
第二步:新建Maven項目ModelB。
ModelB項目依賴ModelA的jar包,並且實現SPIService接口。
package com.starry.service.impl;
import com.starry.service.SPIService;
public class ModelBSPIServiceImpl implements SPIService {
@Override
public String spiService(){
return "model B spiService...";
}
}
與此同時,在resources目錄下新建名爲META-INF/services的package,並新建名爲“com.starry.service.SPIService”的文件,文件內容則爲“com.starry.service.impl.ModelBSPIServiceImpl”,即實現類的類路徑名。
項目目錄結構如下圖所示:
通過maven install命令對ModelB項目打包並且上傳至本地倉庫,供ModelA模塊依賴使用。
第三步:新建Maven項目ModelC。
ModelC項目依賴ModelA的jar包,並且實現SPIService接口。
package com.starry.service.impl;
import com.starry.service.SPIService;
public class ModelCSPIServiceImpl implements SPIService {
@Override
public String spiService(){
return "model c spiService...";
}
}
同樣在resources目錄下新建名爲META-INF/services的package,並新建名爲“com.starry.service.SPIService”的文件,文件內容則爲“com.starry.service.impl.ModelCSPIServiceImpl”,即實現類的類路徑名。
項目的目錄結構如下圖所示:
通過maven install命令對ModelC項目打包並且上傳至本地倉庫,供ModelA模塊依賴使用。
第四步:ModelA模塊依賴ModelB和ModelC,並且尋找到SPIService接口的實現。
ModeA模塊的pom.xml增加依賴:
<dependencies>
<dependency>
<groupId>Model-B</groupId>
<artifactId>Model-B</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>Model-C</groupId>
<artifactId>Model-C</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
測試類:
package com.starry.demo;
import com.starry.service.SPIService;
import java.util.Iterator;
import java.util.ServiceLoader;
public class Client {
public static void main(String[] args) {
ServiceLoader<SPIService> loader = ServiceLoader.load(SPIService.class);
Iterator<SPIService> serviceIterator = loader.iterator();
while(serviceIterator.hasNext()) {
SPIService service = serviceIterator.next();
System.out.println(service.spiService());
}
}
}
很明顯,發現服務實現類的重要工具類是 ServiceLoader,表面現象是通過load(SPIService.class)方法獲取服務接口的實現類,然後通過iterator()方法枚舉服務實現類。
這裏我們不禁會想以下兩個問題:
- ServiceLoader類如何發現服務接口的實現類?
- ServiceLoader類是如何對接口實現類進行加載並且實例化的?
帶着這兩個問題,深入分析ServiceLoader類的源碼。
3.SPI原理分析
針對Client類中的代碼深入分析。
步驟1:
ServiceLoader<SPIService> loader = ServiceLoader.load(SPIService.class);
代碼表面意思是通過ServiceLoader類的load方法返回SPIService接口的加載器;
調用鏈路:
load(Class)-load(Class,ClassLoader)-ServiceLoader(Class,ClassLoader)-reload()
整條鏈路完成下面三件事:
- a. 生成SPIService接口的類加載器serviceLoader對象;
- b. 清理providers的內容;providers是一個有順序的映射,作用是緩存服務實現的對象;此處重新生成服務類的加載器會清理緩存。
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
- c. 生成LazyIterator類的對象lookupIterator;LazyIterator類是延遲迭代器,顧名思義迭代器的內容只有在迭代過程中才會產生;主要邏輯在這個類裏面;
步驟2:
Iterator<SPIService> serviceIterator = loader.iterator();
基於步驟1產生的服務加載器loader對象產生迭代對象serviceIterator。
方法內部返回一個iterator對象,主要實現hasNext()和next()方法。注意步驟1中providers對象剛剛被clear掉,所以此刻knownProviders對象也爲空。
步驟3:
while(serviceIterator.hasNext())
方法調用鏈比較長,避免混亂,先給出整體調用鏈和方法調用結果:
Iterator.hasNext()-LazyIterator.hasNext()-LazyIterator.hasNextService()-lazyIterator.parse(Class,URL)
方法主要完成以下幾件事:
- a. 根據約定(PREFIX常量值)和服務接口的路徑名組成文件資源的路徑fileName;
- b. 通過loader加載器獲取fileName路徑下的文件資源configs;
- c. 按行讀取configs對象的字符流,並存儲在迭代器pending內;
- d. 循環獲取迭代器pending的內容,即服務實現類的類路徑名;
感興趣的可以看一下下面的具體調用過程,感覺枯燥的可以直接看步驟4。
很明顯,當providers對象爲空時,主要的邏輯都由lookupIterator對象完成。因此調用LazyIterator類的hashNext()方法。
此時acc對象爲null,調用LazyIterator類的hasNextService();該方法是核心方法,單獨擰出來分析一下。
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
// 類加載器獲取jar包指定路徑下的文件資源
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 按行讀取configs文件資源內的內容,並且作爲String放置在迭代器pending中
pending = parse(service, configs.nextElement());
}
// 獲取迭代器中內容
nextName = pending.next();
return true;
}
看到PREFIX常量的值(META-INF/services/)就很熟悉,方法會把PREFIX和接口的路徑名組合成一個文件資源路徑fullName,通過類加載的getResources()方法去獲取該路徑下的資源文件,然後通過parse(Class,URL)獲取文件資源內的內容,我們知道其內容爲服務實現類路徑名;所以既然能夠自動獲取到實現類的路徑名,那麼就可以利用反射生成服務實現類的對象;一切都是那麼的順其自然。
步驟4:
SPIService service = serviceIterator.next();
分析這麼久,該方法終於獲取到服務實現類的對象了。方法調用鏈:
Iterator.next()-LazyIterator.next()-LazyIterator.nextService()
具體過程如下:
目前爲止,providers依舊爲空,所以調用lookupIterator對象的next()方法。
調用nextService()方法,該方法也比較核心,所以單獨擰出來分析。
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName; // nextName爲pending迭代器中的第一個內容,即遍歷的首個服務實現類名
nextName = null;
Class<?> c = null;
try {
// 通過反射獲取該服務實現類的class對象
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());
// 以類名爲k,以對象爲v,放入map;放入本地緩存供後面使用
providers.put(cn, p);
return p; // 返回服務實現類的對象
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
方法實現生成服務實現類對象以及把對象緩存到本地內存兩大功能。
步驟5:調用對象的方法。
4.總結
到這裏整個調用的過程分析完畢,可以總結下SPI的機制:SPI機制有一個約定,即模塊B和模塊C實現模塊A提供的Service接口,那麼模塊B和模塊C在其jar包的META-INF/services/目錄下創建一個以服務接口命名的文件,該文件內容就是實現該接口的實現類的路徑名;當模塊A依賴模塊B和模塊C時,就能通過jar包META-INF/services/目錄下的文件找到實現類的類名,然後通過反射實現類的裝載以及實例化,並完成模塊的注入。通過這種尋找接口實現類的機制,可以實現模塊間的解耦。
SPI是一個很巧妙的設計,模塊A在毫無感知的情況下能夠獲取模塊B和模塊C對SPIService接口的實現類對象,而這僅僅依靠一個“約定”,即掃描META-INF/services/目錄下的文件,文件名則爲服務接口的路徑名,文件內容放置實現類的類路徑名即可。“約定大於配置”的思想,在Spring框架中也有很強的體現。但SPI也有缺點,無法精準獲取服務實現類的對象
,無法控制服務實現類實例化
以及SPI的非線程安全
等;針對這些問題,Dubbo在SPI的基礎上改善後得以解決。
參考資料:
http://www.spring4all.com/article/260
https://www.jianshu.com/p/46b42f7f593c