您好,登錄后才能下訂單哦!
Dubbo是一款開源的、高性能且輕量級的Java RPC框架,它提供了三大核心能力:面向接口的遠程方法調用、智能容錯和負載均衡,以及服務自動注冊和發現。
Dubbo最早是阿里公司內部的RPC框架,于 2011 年開源,之后迅速成為國內該類開源項目的佼佼者,2018年2月,通過投票正式成為 Apache基金會孵化項目。目前宜信公司內部也有不少項目在使用Dubbo。
本系列文章通過拆解Dubbo源碼,幫助大家了解Dubbo,做到知其然,并且知其所以然。
SPI(Service Provider Interface),即服務提供方接口,是JDK內置的一種服務提供機制。在寫程序的時候,一般都推薦面向接口編程,這樣做的好處是:降低了程序的耦合性,有利于程序的擴展。
SPI也秉承這種理念,提供了統一的服務接口,服務提供商可以各自提供自己的具體實現。大家都熟知的JDBC中用的就是基于這種機制來發現驅動提供商,不管是Oracle也好,MySQL也罷,在編寫代碼時都一樣,只不過引用的jar包不同而已。后來這種理念也被運用于各種架構之中,比如Dubbo、Eleasticsearch。
SPI 的實現方式是將接口實現類的全限定名配置在文件中,由服務加載器讀取配置文件,加載實現類。
了解了概念后,來看一個具體的例子。
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)添加一個配置文件
在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
例子很簡單,實現的話,可以大膽猜測一下,看名字“ServiceLoader”應該就是用類加載器根據接口的類型加上配置文件里的具體實現名字將實現加載了進來。
接下來通過分析源碼進一步了解其實現原理。
PREFIX定義了加載路徑,reload方法初始化了LazyIterator,LazyIterator是加載的核心,真正實現了加載。加載的模式從名字上就可以看出,是懶加載的模式,只有當真正調用迭代時才會加載。
LazyIterator中的hasNextService方法負責加載配置文件和解析具體的實現類名。
LazyIterator中的nextService方法負責用反射加載實現類。
看完了源碼,感覺這個代碼是有優化空間的,實例化所有實現其實沒啥必要,一來比較耗時,二來浪費資源。Dubbo就沒有使用Java原生的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
上面的測試例子也很簡單,和JDK原生的SPI對比來看,Dubbo的SPI可以根據配置的kv值來獲取。在沒有拆解源碼之前,考慮一下如何實現。
我可能會用雙層Map來實現緩存:第一層的key為接口的class對象,value為一個map;第二層的key為擴展名(配置文件中的key),value為實現類的class。實現懶加載的方式,當運行方法的時候創建空map。在真正獲取時先從緩存中查找具體實現類的class對象,找得到就直接返回、找不到就根據配置文件加載并緩存。
Dubbo又是如何實現的呢?
首先來拆解getExtensionLoader方法。
這是一個靜態的工廠方法,要求傳入的類型必須為接口并且有SPI的注解,用map做了個緩存,key為接口的class對象,而value是 ExtensionLoader對象。
再來拆解ExtensionLoader的getExtension方法。
這段代碼也不復雜,如果傳入的參數為'true',則返回默認的擴展類實例;否則,從緩存中獲取實例,如果有就從緩存中獲取,沒有的話就新建。用map做緩存,緩存了holder對象,而holder對象中存放擴展類。用volatile關鍵字和雙重檢查來應對多線程創建問題,這也是單例模式的常用寫法。
重點分析createExtension方法。
這段代碼由幾部分組成:
第二個沒啥好說的,我們重點來分析一下1、3、4三個部分。
1)getExtensionClasses方法
老套路,從緩存獲取,沒有的話創建并加入緩存。這里緩存的是一個擴展名和class的關系。這個擴展名就是在配置文件中的key。創建之前,先緩存了一下接口的限定名。加載配置文件的路徑是以下這幾個。
2)loadDirectory方法
獲取配置文件路徑,獲取classLoader,并使用loadResource方法做進一步處理。
3)loadResource方法
loadResource加載了配置文件,并解析了配置文件中的內容。loadClass 方法操作了不同的緩存。
首先判斷是否有Adaptive注解,有的話緩存到cacheAdaptiveClass(緩存結構為class);然后判斷是否wrapperclasses,是的話緩存到cacheWrapperClass中(緩存結構為Set);如果以上都不是,這個類就是個普通的類,存儲class和名稱的映射關系到cacheNames里(緩存結構為Map)。
基本上getExtensionClasses方法就分析完了,可以看出來,其實并不是很復雜。
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中獲取對應的實例。先根據名稱查找,找不到的話,再根據類型查找。
依賴注入的部分也拆解完畢,看看這次拆解的最后一部分代碼。
創建wrapper對象的部分,wrapper對象是從哪里來的呢?還記得之前拆解的第一步么,loadClass方法中有幾個緩存,其中wrapperclasses就是緩存這些wrapper的class。
從代碼中可以看出,只要構造方法里有且只有唯一參數,同時此參數為當前傳入的接口類型,即為wrapper class。
此處循環創建wrapper實例,首先將instance做為構造函數的參數,通過反射來創建wrapper對象,然后再向wrapper中注入依賴。
看到這里,可能會有人有疑問:為什么要創建一個wrapper對象?其實很簡單,系統要在真正調用的前后干點別的事唄。這個就有點類似于spring的aop了。
本文簡單介紹了JDK的SPI和Dubbo的SPI用法,分析了JDK的SPI源碼和Dubbo的SPI源碼。在拆解的過程中可以看出,Dubbo的源碼還是很值得一讀的。在實現方面考慮得很周全,不僅有對多線程的處理、多層緩存,也有IOC、AOP的過程。不過,Dubbo的SPI就這么簡單么?當然不是,這篇只拆解了擴展類的加載過程,Dubbo的SPI中還有個很復雜的擴展點-自適應機制。欲知后事如何,請聽下回分解~~
本文作者:宜信支付結算部支付研發團隊Java研發高級工程師鄭祥斌
原文首發于「野指針」
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。