溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

發(fā)布時(shí)間:2020-05-25 15:35:17 來(lái)源:億速云 閱讀:250 作者:鴿子 欄目:軟件技術(shù)

一、JDK SPI

1.1 什么是SPI?

SPI(Service Provider Interface),即服務(wù)提供方接口,是JDK內(nèi)置的一種服務(wù)提供機(jī)制。在寫(xiě)程序的時(shí)候,一般都推薦面向接口編程,這樣做的好處是:降低了程序的耦合性,有利于程序的擴(kuò)展。

SPI也秉承這種理念,提供了統(tǒng)一的服務(wù)接口,服務(wù)提供商可以各自提供自己的具體實(shí)現(xiàn)。大家都熟知的JDBC中用的就是基于這種機(jī)制來(lái)發(fā)現(xiàn)驅(qū)動(dòng)提供商,不管是Oracle也好,MySQL也罷,在編寫(xiě)代碼時(shí)都一樣,只不過(guò)引用的jar包不同而已。后來(lái)這種理念也被運(yùn)用于各種架構(gòu)之中,比如Dubbo、Eleasticsearch。

1.2 JDK SPI的小栗子

SPI 的實(shí)現(xiàn)方式是將接口實(shí)現(xiàn)類(lèi)的全限定名配置在文件中,由服務(wù)加載器讀取配置文件,加載實(shí)現(xiàn)類(lèi)。

了解了概念后,來(lái)看一個(gè)具體的例子。

1)定義一個(gè)接口

public interface Operation {
        int operate(int num1, int num2);
}

2)寫(xiě)兩個(gè)簡(jiǎn)單的實(shí)現(xiàn)

public class DivisionOperation implements Operation {
        public int operate(int num1, int num2) {
            System.out.println("run division operation");
            return num1/num2;
        }
}

3)添加一個(gè)配置文件

在ClassPath路徑下添加一個(gè)配置文件,文件名字是接口的全限定類(lèi)名,內(nèi)容是實(shí)現(xiàn)類(lèi)的全限定類(lèi)名,多個(gè)實(shí)現(xiàn)類(lèi)用換行符分隔。

目錄結(jié)構(gòu)

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

文件內(nèi)容

com.api.impl.DivisionOperation
com.api.impl.PlusOperation

4)測(cè)試程序

public class JavaSpiTest {
    @Test
    public void testOperation() throws Exception {
        ServiceLoader<Operation> operations = ServiceLoader.load(Operation.class);
        operations.forEach(item->System.out.println("result: " + item.operate(2, 2)));
    } 
}

5)測(cè)試結(jié)果

run division operation
result:1
run plus operation
result:4

1.3 JDK SPI的源碼分析

例子很簡(jiǎn)單,實(shí)現(xiàn)的話,可以大膽猜測(cè)一下,看名字“ServiceLoader”應(yīng)該就是用類(lèi)加載器根據(jù)接口的類(lèi)型加上配置文件里的具體實(shí)現(xiàn)名字將實(shí)現(xiàn)加載了進(jìn)來(lái)。

接下來(lái)通過(guò)分析源碼進(jìn)一步了解其實(shí)現(xiàn)原理。

1.3.1 ServiceLoader類(lèi)

PREFIX定義了加載路徑,reload方法初始化了LazyIterator,LazyIterator是加載的核心,真正實(shí)現(xiàn)了加載。加載的模式從名字上就可以看出,是懶加載的模式,只有當(dāng)真正調(diào)用迭代時(shí)才會(huì)加載。

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

1.3.2 hasNextService方法

LazyIterator中的hasNextService方法負(fù)責(zé)加載配置文件和解析具體的實(shí)現(xiàn)類(lèi)名。

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

1.3.3 nextService方法

LazyIterator中的nextService方法負(fù)責(zé)用反射加載實(shí)現(xiàn)類(lèi)。

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

看完了源碼,感覺(jué)這個(gè)代碼是有優(yōu)化空間的,實(shí)例化所有實(shí)現(xiàn)其實(shí)沒(méi)啥必要,一來(lái)比較耗時(shí),二來(lái)浪費(fèi)資源。Dubbo就沒(méi)有使用Java原生的SPI機(jī)制,而是對(duì)其進(jìn)行了增強(qiáng),使其能夠更好地滿足需求。

二、Dubbo SPI

2.1 Dubbo SPI的小栗子

老習(xí)慣,在拆解源碼之前,先來(lái)個(gè)栗子。此處示例是在前文例子的基礎(chǔ)上稍做了些修改。

1)定義一個(gè)接口

修改接口,加上了Dubbo的@SPI注解。

@SPI
public interface Operation {
        int operate(int num1, int num2);
}

2)寫(xiě)兩個(gè)簡(jiǎn)單的實(shí)現(xiàn)

沿用之前的兩個(gè)實(shí)現(xiàn)類(lèi)。

3)添加一個(gè)配置文件

新增配置文件放在dubbo目錄下。

目錄結(jié)構(gòu)

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

文件內(nèi)容

division=com.api.impl.DivisionOperation
plus=com.api.impl.PlusOperation

4)測(cè)試程序

public class DubboSpiTest {
    @Test
    public void testOperation() throws Exception {
          ExtensionLoader<Operation> loader = ExtensionLoader.getExtensionLoader(Operation.class);
          Operation division = loader.getExtension("division");
          System.out.println("result: " + division.operate(1, 2));
    } 
}

5)測(cè)試結(jié)果

run division operation
result:0

2.2 Dubbo SPI源碼

上面的測(cè)試?yán)右埠芎?jiǎn)單,和JDK原生的SPI對(duì)比來(lái)看,Dubbo的SPI可以根據(jù)配置的kv值來(lái)獲取。在沒(méi)有拆解源碼之前,考慮一下如何實(shí)現(xiàn)。

我可能會(huì)用雙層Map來(lái)實(shí)現(xiàn)緩存:第一層的key為接口的class對(duì)象,value為一個(gè)map;第二層的key為擴(kuò)展名(配置文件中的key),value為實(shí)現(xiàn)類(lèi)的class。實(shí)現(xiàn)懶加載的方式,當(dāng)運(yùn)行方法的時(shí)候創(chuàng)建空map。在真正獲取時(shí)先從緩存中查找具體實(shí)現(xiàn)類(lèi)的class對(duì)象,找得到就直接返回、找不到就根據(jù)配置文件加載并緩存。

Dubbo又是如何實(shí)現(xiàn)的呢?

2.2.1 getExtensionLoader方法

首先來(lái)拆解getExtensionLoader方法。

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

這是一個(gè)靜態(tài)的工廠方法,要求傳入的類(lèi)型必須為接口并且有SPI的注解,用map做了個(gè)緩存,key為接口的class對(duì)象,而value是 ExtensionLoader對(duì)象。

2.2.2 getExtension方法

再來(lái)拆解ExtensionLoader的getExtension方法。

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

這段代碼也不復(fù)雜,如果傳入的參數(shù)為'true',則返回默認(rèn)的擴(kuò)展類(lèi)實(shí)例;否則,從緩存中獲取實(shí)例,如果有就從緩存中獲取,沒(méi)有的話就新建。用map做緩存,緩存了holder對(duì)象,而holder對(duì)象中存放擴(kuò)展類(lèi)。用volatile關(guān)鍵字和雙重檢查來(lái)應(yīng)對(duì)多線程創(chuàng)建問(wèn)題,這也是單例模式的常用寫(xiě)法。

2.2.3 createExtension方法

重點(diǎn)分析createExtension方法。

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

這段代碼由幾部分組成:

  • 根據(jù)傳入的擴(kuò)展名獲取對(duì)應(yīng)的class。
  • 根據(jù)class去緩存中獲取實(shí)例,如果沒(méi)有的話,通過(guò)反射創(chuàng)建對(duì)象并放入緩存。
  • 依賴注入,完成對(duì)象實(shí)例的初始化。
  • 創(chuàng)建wrapper對(duì)象。也就是說(shuō),此處返回的對(duì)象不一定是具體的實(shí)現(xiàn)類(lèi),可能是包裝的對(duì)象。

第二個(gè)沒(méi)啥好說(shuō)的,我們重點(diǎn)來(lái)分析一下1、3、4三個(gè)部分。

1)getExtensionClasses方法

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

老套路,從緩存獲取,沒(méi)有的話創(chuàng)建并加入緩存。這里緩存的是一個(gè)擴(kuò)展名和class的關(guān)系。這個(gè)擴(kuò)展名就是在配置文件中的key。創(chuàng)建之前,先緩存了一下接口的限定名。加載配置文件的路徑是以下這幾個(gè)。

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

2)loadDirectory方法

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

獲取配置文件路徑,獲取classLoader,并使用loadResource方法做進(jìn)一步處理。

3)loadResource方法

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

loadResource加載了配置文件,并解析了配置文件中的內(nèi)容。loadClass 方法操作了不同的緩存。

首先判斷是否有Adaptive注解,有的話緩存到cacheAdaptiveClass(緩存結(jié)構(gòu)為class);然后判斷是否wrapperclasses,是的話緩存到cacheWrapperClass中(緩存結(jié)構(gòu)為Set);如果以上都不是,這個(gè)類(lèi)就是個(gè)普通的類(lèi),存儲(chǔ)class和名稱(chēng)的映射關(guān)系到cacheNames里(緩存結(jié)構(gòu)為Map)。

基本上getExtensionClasses方法就分析完了,可以看出來(lái),其實(shí)并不是很復(fù)雜。

2.2.4 IOC

1)injectExtension方法

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

這個(gè)方法實(shí)現(xiàn)了依賴注入,即IOC。首先通過(guò)反射獲取到實(shí)例的方法;然后遍歷,獲取setter方法;接著從objectFactory中獲取依賴對(duì)象;最后通過(guò)反射調(diào)用setter方法注入依賴。

objectFactory的變量類(lèi)型為AdaptiveExtensionFactory。

2)AdaptiveExtensionFactory

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

這個(gè)類(lèi)里面有個(gè)ExtensionFactory的列表,用來(lái)存儲(chǔ)其他類(lèi)型的 ExtensionFactory。Dubbo提供了兩種ExtensionFactory,一種是SpiExtensionFactory, 用于創(chuàng)建自適應(yīng)的擴(kuò)展;另一種是SpringExtesionFactory,用于從Spring的IOC容器中獲取擴(kuò)展。配置文件一個(gè)在dubbo-common模塊,一個(gè)在dubbo-config模塊。

配置文件

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

SpiExtensionFactory中的Spi方式前面已經(jīng)解析過(guò)了。

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

SpringExtesionFactory是從ApplicationContext中獲取對(duì)應(yīng)的實(shí)例。先根據(jù)名稱(chēng)查找,找不到的話,再根據(jù)類(lèi)型查找。

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

依賴注入的部分也拆解完畢,看看這次拆解的最后一部分代碼。

2.2.5 AOP

創(chuàng)建wrapper對(duì)象的部分,wrapper對(duì)象是從哪里來(lái)的呢?還記得之前拆解的第一步么,loadClass方法中有幾個(gè)緩存,其中wrapperclasses就是緩存這些wrapper的class。

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

從代碼中可以看出,只要構(gòu)造方法里有且只有唯一參數(shù),同時(shí)此參數(shù)為當(dāng)前傳入的接口類(lèi)型,即為wrapper class。

Dubbo SPI擴(kuò)展類(lèi)的加載過(guò)程

此處循環(huán)創(chuàng)建wrapper實(shí)例,首先將instance做為構(gòu)造函數(shù)的參數(shù),通過(guò)反射來(lái)創(chuàng)建wrapper對(duì)象,然后再向wrapper中注入依賴。

看到這里,可能會(huì)有人有疑問(wèn):為什么要?jiǎng)?chuàng)建一個(gè)wrapper對(duì)象?其實(shí)很簡(jiǎn)單,系統(tǒng)要在真正調(diào)用的前后干點(diǎn)別的事唄。這個(gè)就有點(diǎn)類(lèi)似于spring的aop了。

三、總結(jié)

本文簡(jiǎn)單介紹了JDK的SPI和Dubbo的SPI用法,分析了JDK的SPI源碼和Dubbo的SPI源碼。在拆解的過(guò)程中可以看出,Dubbo的源碼還是很值得一讀的。在實(shí)現(xiàn)方面考慮得很周全,不僅有對(duì)多線程的處理、多層緩存,也有IOC、AOP的過(guò)程。不過(guò),Dubbo的SPI就這么簡(jiǎn)單么?當(dāng)然不是,這篇只拆解了擴(kuò)展類(lèi)的加載過(guò)程,Dubbo的SPI中還有個(gè)很復(fù)雜的擴(kuò)展點(diǎn)-自適應(yīng)機(jī)制。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI