5. SPI 機(jī)制原理
因為dubbo 框架是建立的 SPI 機(jī)制上,因此在探尋 dubbo 框架源碼前,我們需要先把 SPI 機(jī)制了解透徹。
5.1 java spi 機(jī)制
SPI 全稱 Service Provider Interface,是 Java 提供的一套用來被第三方實現(xiàn)或者擴(kuò)展的 API,它可以用來啟用框架擴(kuò)展和替換組件。

可以看到,SPI 的本質(zhì),其實是幫助程序,為某個特定的接口尋找它的實現(xiàn)類。而且哪些實現(xiàn)類的會加載,是個動態(tài)過程(不是提前預(yù)定好的)。
有點類似 IOC 的思想,就是將裝配的控制權(quán)移到程序之外,在模塊化設(shè)計中這個機(jī)制尤其重要。所以 SPI 的核心思想就是解耦。
比較常見的例子:
? 數(shù)據(jù)庫驅(qū)動加載接口實現(xiàn)類的加載:
????JDBC 加載不同類型數(shù)據(jù)庫的驅(qū)動
? 日志門面接口實現(xiàn)類加載:
????SLF4J 加載不同提供商的日志實現(xiàn)類
? Spring:
????Spring 中大量使用了 SPI,比如:對 servlet3.0 規(guī)范對
????ServletContainerInitializer 的實現(xiàn)、自動類型轉(zhuǎn)換 Type Conversion
????SPI(Converter SPI、Formatter SPI)等
5.1.1 使用介紹
要使用 Java SPI,需要遵循如下約定:
? 1、當(dāng)服務(wù)提供者提供了接口的一種具體實現(xiàn)后,在 jar 包的META-INF/services 目錄下創(chuàng)建一個以“接口全限定名”為命名的文件,內(nèi)容為實現(xiàn)類的全限定名;
? 2、接口實現(xiàn)類所在的 jar 包放在主程序的 classpath 中;
? 3、主程序通過 java.util.ServiceLoder 動態(tài)裝載實現(xiàn)模塊,它通過掃描META-INF/services 目錄下的配置文件找到實現(xiàn)類的全限定名,把類加載到 JVM;
? 4、SPI 的實現(xiàn)類必須攜帶一個不帶參數(shù)的構(gòu)造方法。
示例:先定義一個接口(代碼示例見源碼包)
public interface InfoService {
????Object sayHello(String name) ;
}
再定義一系列它的實現(xiàn),第一個實現(xiàn):
public class InfoServiceAImpl implements InfoService {
????@Override
????public Object sayHello(String name) {
????System.out.println(name+",你好,調(diào)通了 A 實現(xiàn)!");
????return name+",你好,調(diào)通了 A 實現(xiàn)!";
????}
}
第二個實現(xiàn):
public class InfoServiceBImpl implements InfoService {
????@Override
????public Object sayHello(String name) {
????System.out.println(name+",你好,調(diào)通了 B 實現(xiàn)!");
????return name+",你好,調(diào)通了 B 實現(xiàn)!";
????}
}
...等等實現(xiàn)

至此,整個 java SPI 的機(jī)制使用介紹完畢。
5.1.2 核心功能類
需要指出的是,java 之所以能夠順利根據(jù)配置加載這個實現(xiàn)類,完全依賴于jdk 內(nèi)的一個核心類:

5.2 Dubbo SPI 機(jī)制
在上一節(jié)中,可以看到,java spi 機(jī)制非常簡單,就是讀取指定的配置文件,將所有的類都加載到程序中。而這種機(jī)制,存在很多缺陷,比如:
1. 所有實現(xiàn)類無論是否使用,直接被加載,可能存在浪費;
2. 不能夠靈活控制什么時候什么時機(jī),匹配什么實現(xiàn),功能太弱Dubbo 基于自己的需要,增強(qiáng)了這套 SPI 機(jī)制,下面介紹 Dubbo 中的 SPI用法。
5.2.1? 標(biāo)簽@SPI 用法
與 Java SPI 實現(xiàn)類配置不同,Dubbo SPI 是通過鍵值對的方式進(jìn)行配置,這樣我們就可以按需加載指定的實現(xiàn)類。另外,需要在接口上標(biāo)注 @SPI 注解。表明此接口是 SPI 的擴(kuò)展點:
@SPI("b")
public interface InfoService {
????Object sayHello(String name) ;
????Object passInfo(String msg, URL url) ;
}
dubbo 的配置文件夾路徑:

配置文件內(nèi)容:

測試功能,指定加載某個實現(xiàn):

測試結(jié)果:

5.2.2? 標(biāo)簽@Activate 用法
Dubbo 的 SPI?機(jī)制雖然對原生 SPI 有了增強(qiáng),但功能還遠(yuǎn)遠(yuǎn)不夠。
在工作中,某種時候存在這樣的情形,需要同時啟用某個接口的多個實現(xiàn)類,如 Filter 過濾器。我們希望某種條件下啟用這一批實現(xiàn),而另一種情況下啟用那一批實現(xiàn),比如:希望的 RPC 調(diào)用的消費端和服務(wù)端,分別啟用不同的兩批 Filter,怎么處理呢?
????這時我們的條件激活注解@Activate,就派上了用場。
Activate 注解表示一個擴(kuò)展是否被激活(使用),可以放在類定義和方法上,dubbo 用它在 spi 擴(kuò)展類定義上,表示這個擴(kuò)展實現(xiàn)激活條件和時機(jī)。它有兩個設(shè)置過濾條件的字段,group,value 都是字符數(shù)組。?
用來指定這個擴(kuò)展類在什么條件下激活。
下面以 com.alibaba.dubbo.rpc.filter 接口的幾個擴(kuò)展來說明。
@Activate(group = {Constants.PROVIDER, Constants.CONSUMER})
?public class testActivate1 implements Filter {
}
//表示如果過濾器使用方(通過 group 指定)屬于 Constants.PROVIDER(服務(wù)提供方)或者 Constants.CONSUMER(服務(wù)消費方)就激活使用這個過濾器
//再看這個擴(kuò)展
@Activate(group = Constants.PROVIDER, value = Constants.TOKEN_KEY)
public class testActivate2 implements Filter {
}
//表示如果過濾器使用方(通過 group 指定)屬于 Constants.PROVIDER(服務(wù)提供方)并且 URL 中有參數(shù) Constants.TOKEN_KEY(token)時就激活使用這個過濾器
詳細(xì)用法見源碼 demo。
5.2.3 javassist 動態(tài)編譯
在 SPI 尋找實現(xiàn)類的過程中,getAdaptiveExtension 方法得到的對象,只是個接口代理對象,此代理對象是由臨時編譯的類來實現(xiàn)的。在此,先說明一個 javassist 動態(tài)編譯類的兩種用法:
通過創(chuàng)建 class 模型對象設(shè)置 class 屬性,然后生成 Class:
1. CtClass-->CtField-->CtMethod
2. Class clazz = ctClass.toClass()
直接編譯拼湊好的定義 class 的字符串,來生成 class:
JavassistCompiler.compile("public class DemoImpl
implements DemoService {...}",ClassLoader());
5.2.4 標(biāo)簽@Adaptive 用法
我們在前面演示了 dubbo SPI 的使用,但是有一個問題,擴(kuò)展點對應(yīng)的實現(xiàn)類不能在程序運行時動態(tài)指定,就是 extensionLoader.getExtension 方法寫死了擴(kuò)展點對應(yīng)的實現(xiàn)類,不能在程序運行期間根據(jù)運行時參數(shù)進(jìn)行動態(tài)改變。
而我們希望在程序使用時,對實現(xiàn)類進(jìn)行懶加載,并且能根據(jù)運行時情況來決定,應(yīng)該啟用哪個擴(kuò)展類。為了解決這個問題,dubbo 引入了 Adaptive注解,也就是 dubbo 的自適應(yīng)機(jī)制。
先看下面的示例:
public void test3(){
????ExtensionLoader<InfoService> loader =
????ExtensionLoader.getExtensionLoader(InfoService.class);
????InfoService adaptiveExtension = loader.getAdaptiveExtension();
????URL url = URL.valueOf("test://localhost/test?info.service=a");
????System.out.println(adaptiveExtension.passInfo("james", url));
}
我們的 InfoService 的 passInfo 方法參數(shù)內(nèi),有一個 URL 的參數(shù)。URL 中附帶了信息 info.service=a,希望調(diào)用 a 實現(xiàn)。a 實現(xiàn)的配置如下:

這初看起來非常矛盾,都已經(jīng)在調(diào)用 InfoService 對象了,怎么還有機(jī)會來選擇調(diào)用哪個 InfoService 對象呢?
其實重點就在于,現(xiàn)在的 InfoService 的調(diào)用對象 adaptiveExtension ,在當(dāng)前,還只是個代理類,因此我們還有在代理內(nèi)選擇哪個目標(biāo)實現(xiàn)的機(jī)會。????

我們運行代碼,會發(fā)現(xiàn)還真就是調(diào)用的 A 實現(xiàn)類使用重點,URL 的格式:info.service=a 的參數(shù)名格式,是接口類 InfoService 的駝峰大小寫拆分。
5.2.5 Dubbo SPI 的依賴注入
Dubbo SPI 的核心實現(xiàn)類為 ExtensionLoader,此類的使用幾乎遍及 Dubbo 的整個源碼體系。是大家以傳統(tǒng)方式讀源碼的嚴(yán)重障礙。
ExtensionLoader 有三個重要的入口方法,分別與@SPI、@Activate、@Adaptive 注解對應(yīng)。
getExtension 方法,對應(yīng)加載所有的實現(xiàn)
getActivateExtension 方法,對應(yīng)解析加載@Activate 注解對應(yīng)的實現(xiàn)
getAdaptiveExtension 方法,對應(yīng)解析加載@Adaptive 注解對應(yīng)的實現(xiàn)
其中,@Adaptive 注解作的自適應(yīng)功能,還涉及到了代理對象(而 Dubbo 的代理機(jī)制,有兩種選擇,jdk 動態(tài)代理和 javassist 動態(tài)編譯類)。我們將后后續(xù)篇章對此進(jìn)行說明。
Dubbo 的 SPI 機(jī)制,除上以上三種注解的用法外,還有一個重要的功能依賴注入對于 spring 這個強(qiáng)大的 IOC 工具,依賴注入大家一定都很了妥!在 Dubbo 自動生成 SPI 的擴(kuò)展實例的時候也會發(fā)生依賴注入的場景,舉一個具體的例子。
現(xiàn)有一個接口擴(kuò)展點:
@SPI("peter")
public interface OrderService {
????String getDetail(String id, URL url);
}
而其實現(xiàn)類中,引入了另一個擴(kuò)展點接口對象 infoService:
public class OrderServiceImpl implements OrderService {
????private InfoService infoService;
????public void setInfoService(InfoService infoService) {
????this.infoService = infoService;
}
@Override
public String getDetail(String name, URL url) {
????infoService.passInfo(name,url);
????System.out.println(name+",訂單處理成功!");
????return name+",你好,訂單處理成功!";
????}
}
此時,假如我們的 OrderService 在加載時會發(fā)生什么呢?
@Test
public void iocSPI() {
????//獲取 OrderService 的 Loader 實例
????ExtensionLoader<OrderService> extensionLoader =
????ExtensionLoader.getExtensionLoader(OrderService.class);
????//取得默認(rèn)拓展類
????OrderService orderService = extensionLoader.getDefaultExtension();
????URL url = URL.valueOf("test://localhost/test?info.service=a");
????orderService.getDetail("peter",url);
}
結(jié)果如下:

可以看到,dubbo 自動生成了實例,并注入了依賴之中。
這是什么機(jī)理呢,看這個擴(kuò)展接口:

這是 dubbo 的一個擴(kuò)展點工廠接口,只有一個方法,根據(jù) class 和 name 查找實現(xiàn)。
這個接口,是一個擴(kuò)展點,接下來看看此接口的實現(xiàn)類:

可以看到,有兩個實現(xiàn)類:
一個適配類(adaptive,接口的默認(rèn)實現(xiàn)的是AdaptiveExtensionFactory 在內(nèi)部持有了所有的 factory 實現(xiàn)工廠,即后兩個實現(xiàn)類)。
一個為 SPI 工廠(依賴類是擴(kuò)展接口時發(fā)揮作用),一個為 Spring 工廠(依賴的是 springbean 時發(fā)揮作用)。
于是,當(dāng)我們需要為某個生成的對象注入依賴時,直接調(diào)用此對象即可。
至此,整套 DubboSPI 的 IOC 功能圓滿了。