Dubbo|基礎知識之SPI機制

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”,即實現類的類路徑名。

項目目錄結構如下圖所示:
ModelB

通過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”,即實現類的類路徑名。

項目的目錄結構如下圖所示:
ModelC
通過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接口的加載器;
1
2
3
4
調用鏈路
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。
21
方法內部返回一個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。
31
很明顯,當providers對象爲空時,主要的邏輯都由lookupIterator對象完成。因此調用LazyIterator類的hashNext()方法。
32
此時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()
具體過程如下:
41
目前爲止,providers依舊爲空,所以調用lookupIterator對象的next()方法。
42
調用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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章