Dubbo源碼解析之SPI(一):擴展類的加載過程

Dubbo是一款開源的、高性能且輕量級的Java RPC框架,它提供了三大核心能力:面向接口的遠程方法調用、智能容錯和負載均衡,以及服務自動註冊和發現。spring

Dubbo最先是阿里公司內部的RPC框架,於 2011 年開源,以後迅速成爲國內該類開源項目的佼佼者,2018年2月,經過投票正式成爲 Apache基金會孵化項目。目前宜信公司內部也有很多項目在使用Dubbo。編程

本系列文章經過拆解Dubbo源碼,幫助你們瞭解Dubbo,作到知其然,而且知其因此然。api

1、JDK SPI

1.1 什麼是SPI?

SPI(Service Provider Interface),即服務提供方接口,是JDK內置的一種服務提供機制。在寫程序的時候,通常都推薦面向接口編程,這樣作的好處是:下降了程序的耦合性,有利於程序的擴展。緩存

SPI也秉承這種理念,提供了統一的服務接口,服務提供商能夠各自提供本身的具體實現。你們都熟知的JDBC中用的就是基於這種機制來發現驅動提供商,不論是Oracle也好,MySQL也罷,在編寫代碼時都同樣,只不過引用的jar包不一樣而已。後來這種理念也被運用於各類架構之中,好比Dubbo、Eleasticsearch。多線程

1.2 JDK SPI的小栗子

SPI 的實現方式是將接口實現類的全限定名配置在文件中,由服務加載器讀取配置文件,加載實現類。 架構

瞭解了概念後,來看一個具體的例子。app

1)定義一個接口負載均衡

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

2)寫兩個簡單的實現框架

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

3)添加一個配置文件ide

在ClassPath路徑下添加一個配置文件,文件名字是接口的全限定類名,內容是實現類的全限定類名,多個實現類用換行符分隔。

目錄結構

文件內容

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

4)測試程序

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)測試結果

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

1.3 JDK SPI的源碼分析

例子很簡單,實現的話,能夠大膽猜想一下,看名字「ServiceLoader」應該就是用類加載器根據接口的類型加上配置文件裏的具體實現名字將實現加載了進來。

接下來經過分析源碼進一步瞭解其實現原理。

1.3.1 ServiceLoader類

PREFIX定義了加載路徑,reload方法初始化了LazyIterator,LazyIterator是加載的核心,真正實現了加載。加載的模式從名字上就能夠看出,是懶加載的模式,只有當真正調用迭代時纔會加載。

1.3.2 hasNextService方法

LazyIterator中的hasNextService方法負責加載配置文件和解析具體的實現類名。

1.3.3 nextService方法

LazyIterator中的nextService方法負責用反射加載實現類。

看完了源碼,感受這個代碼是有優化空間的,實例化全部實現其實沒啥必要,一來比較耗時,二來浪費資源。Dubbo就沒有使用Java原生的SPI機制,而是對其進行了加強,使其可以更好地知足需求。

2、Dubbo SPI

2.1 Dubbo SPI的小栗子

老習慣,在拆解源碼以前,先來個栗子。此處示例是在前文例子的基礎上稍作了些修改。

1)定義一個接口

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

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

2)寫兩個簡單的實現

沿用以前的兩個實現類。

3)添加一個配置文件

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

目錄結構

文件內容

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

4)測試程序

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)測試結果

run division operation
result:0

2.2 Dubbo SPI源碼

上面的測試例子也很簡單,和JDK原生的SPI對比來看,Dubbo的SPI能夠根據配置的kv值來獲取。在沒有拆解源碼以前,考慮一下如何實現。

我可能會用雙層Map來實現緩存:第一層的key爲接口的class對象,value爲一個map;第二層的key爲擴展名(配置文件中的key),value爲實現類的class。實現懶加載的方式,當運行方法的時候建立空map。在真正獲取時先從緩存中查找具體實現類的class對象,找獲得就直接返回、找不到就根據配置文件加載並緩存。

Dubbo又是如何實現的呢?

2.2.1 getExtensionLoader方法

首先來拆解getExtensionLoader方法。

這是一個靜態的工廠方法,要求傳入的類型必須爲接口而且有SPI的註解,用map作了個緩存,key爲接口的class對象,而value是 ExtensionLoader對象。

2.2.2 getExtension方法

再來拆解ExtensionLoader的getExtension方法。

這段代碼也不復雜,若是傳入的參數爲'true',則返回默認的擴展類實例;不然,從緩存中獲取實例,若是有就從緩存中獲取,沒有的話就新建。用map作緩存,緩存了holder對象,而holder對象中存放擴展類。用volatile關鍵字和雙重檢查來應對多線程建立問題,這也是單例模式的經常使用寫法。

2.2.3 createExtension方法

重點分析createExtension方法。

這段代碼由幾部分組成:

  • 根據傳入的擴展名獲取對應的class。
  • 根據class去緩存中獲取實例,若是沒有的話,經過反射建立對象並放入緩存。
  • 依賴注入,完成對象實例的初始化。
  • 建立wrapper對象。也就是說,此處返回的對象不必定是具體的實現類,多是包裝的對象。

第二個沒啥好說的,咱們重點來分析一下一、三、4三個部分。

1)getExtensionClasses方法

老套路,從緩存獲取,沒有的話建立並加入緩存。這裏緩存的是一個擴展名和class的關係。這個擴展名就是在配置文件中的key。建立以前,先緩存了一下接口的限定名。加載配置文件的路徑是如下這幾個。

2)loadDirectory方法

獲取配置文件路徑,獲取classLoader,並使用loadResource方法作進一步處理。

3)loadResource方法

loadResource加載了配置文件,並解析了配置文件中的內容。loadClass 方法操做了不一樣的緩存。

首先判斷是否有Adaptive註解,有的話緩存到cacheAdaptiveClass(緩存結構爲class);而後判斷是否wrapperclasses,是的話緩存到cacheWrapperClass中(緩存結構爲Set);若是以上都不是,這個類就是個普通的類,存儲class和名稱的映射關係到cacheNames裏(緩存結構爲Map)。

基本上getExtensionClasses方法就分析完了,能夠看出來,其實並非很複雜。

2.2.4 IOC

1)injectExtension方法

這個方法實現了依賴注入,即IOC。首先經過反射獲取到實例的方法;而後遍歷,獲取setter方法;接着從objectFactory中獲取依賴對象;最後經過反射調用setter方法注入依賴。

objectFactory的變量類型爲AdaptiveExtensionFactory。

2)AdaptiveExtensionFactory

這個類裏面有個ExtensionFactory的列表,用來存儲其餘類型的 ExtensionFactory。Dubbo提供了兩種ExtensionFactory,一種是SpiExtensionFactory, 用於建立自適應的擴展;另外一種是SpringExtesionFactory,用於從Spring的IOC容器中獲取擴展。配置文件一個在dubbo-common模塊,一個在dubbo-config模塊。

配置文件

SpiExtensionFactory中的Spi方式前面已經解析過了。

SpringExtesionFactory是從ApplicationContext中獲取對應的實例。先根據名稱查找,找不到的話,再根據類型查找。

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

2.2.5 AOP

建立wrapper對象的部分,wrapper對象是從哪裏來的呢?還記得以前拆解的第一步麼,loadClass方法中有幾個緩存,其中wrapperclasses就是緩存這些wrapper的class。

從代碼中能夠看出,只要構造方法裏有且只有惟一參數,同時此參數爲當前傳入的接口類型,即爲wrapper class。

此處循環建立wrapper實例,首先將instance作爲構造函數的參數,經過反射來建立wrapper對象,而後再向wrapper中注入依賴。

看到這裏,可能會有人有疑問:爲何要建立一個wrapper對象?其實很簡單,系統要在真正調用的先後乾點別的事唄。這個就有點相似於spring的aop了。

3、總結

本文簡單介紹了JDK的SPI和Dubbo的SPI用法,分析了JDK的SPI源碼和Dubbo的SPI源碼。在拆解的過程當中能夠看出,Dubbo的源碼仍是很值得一讀的。在實現方面考慮得很周全,不只有對多線程的處理、多層緩存,也有IOC、AOP的過程。不過,Dubbo的SPI就這麼簡單麼?固然不是,這篇只拆解了擴展類的加載過程,Dubbo的SPI中還有個很複雜的擴展點-自適應機制。欲知後事如何,請聽下回分解~~

來源:宜信技術學院

本文做者:宜信支付結算部支付研發團隊Java研發高級工程師鄭祥斌

原文首發於「野指針」

相關文章
相關標籤/搜索