一、JDK SPI
創(chuàng)新互聯(lián)專注于中大型企業(yè)的成都網(wǎng)站建設(shè)、網(wǎng)站制作和網(wǎng)站改版、網(wǎng)站營銷服務(wù),追求商業(yè)策劃與數(shù)據(jù)分析、創(chuàng)意藝術(shù)與技術(shù)開發(fā)的融合,累計(jì)客戶千余家,服務(wù)滿意度達(dá)97%。幫助廣大客戶順利對(duì)接上互聯(lián)網(wǎng)浪潮,準(zhǔn)確優(yōu)選出符合自己需要的互聯(lián)網(wǎng)運(yùn)用,我們將一直專注成都品牌網(wǎng)站建設(shè)和互聯(lián)網(wǎng)程序開發(fā),在前進(jìn)的路上,與客戶一起成長!1.1 什么是SPI?SPI(Service Provider Interface),即服務(wù)提供方接口,是JDK內(nèi)置的一種服務(wù)提供機(jī)制。在寫程序的時(shí)候,一般都推薦面向接口編程,這樣做的好處是:降低了程序的耦合性,有利于程序的擴(kuò)展。
SPI也秉承這種理念,提供了統(tǒng)一的服務(wù)接口,服務(wù)提供商可以各自提供自己的具體實(shí)現(xiàn)。大家都熟知的JDBC中用的就是基于這種機(jī)制來發(fā)現(xiàn)驅(qū)動(dòng)提供商,不管是Oracle也好,MySQL也罷,在編寫代碼時(shí)都一樣,只不過引用的jar包不同而已。后來這種理念也被運(yùn)用于各種架構(gòu)之中,比如Dubbo、Eleasticsearch。
1.2 JDK SPI的小栗子SPI 的實(shí)現(xiàn)方式是將接口實(shí)現(xiàn)類的全限定名配置在文件中,由服務(wù)加載器讀取配置文件,加載實(shí)現(xiàn)類。
了解了概念后,來看一個(gè)具體的例子。
1)定義一個(gè)接口
public interface Operation {
int operate(int num1, int num2);
}
2)寫兩個(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è)配置文件,文件名字是接口的全限定類名,內(nèi)容是實(shí)現(xiàn)類的全限定類名,多個(gè)實(shí)現(xiàn)類用換行符分隔。
目錄結(jié)構(gòu)
文件內(nèi)容
com.api.impl.DivisionOperation
com.api.impl.PlusOperation
4)測(cè)試程序
public class JavaSpiTest {
@Test
public void testOperation() throws Exception {
ServiceLoader 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)該就是用類加載器根據(jù)接口的類型加上配置文件里的具體實(shí)現(xiàn)名字將實(shí)現(xiàn)加載了進(jìn)來。
接下來通過分析源碼進(jìn)一步了解其實(shí)現(xiàn)原理。
1.3.1 ServiceLoader類PREFIX定義了加載路徑,reload方法初始化了LazyIterator,LazyIterator是加載的核心,真正實(shí)現(xiàn)了加載。加載的模式從名字上就可以看出,是懶加載的模式,只有當(dāng)真正調(diào)用迭代時(shí)才會(huì)加載。
1.3.2 hasNextService方法LazyIterator中的hasNextService方法負(fù)責(zé)加載配置文件和解析具體的實(shí)現(xiàn)類名。
1.3.3 nextService方法LazyIterator中的nextService方法負(fù)責(zé)用反射加載實(shí)現(xiàn)類。
看完了源碼,感覺這個(gè)代碼是有優(yōu)化空間的,實(shí)例化所有實(shí)現(xiàn)其實(shí)沒啥必要,一來比較耗時(shí),二來浪費(fèi)資源。Dubbo就沒有使用Java原生的SPI機(jī)制,而是對(duì)其進(jìn)行了增強(qiáng),使其能夠更好地滿足需求。
二、Dubbo SPI2.1 Dubbo SPI的小栗子老習(xí)慣,在拆解源碼之前,先來個(gè)栗子。此處示例是在前文例子的基礎(chǔ)上稍做了些修改。
1)定義一個(gè)接口
修改接口,加上了Dubbo的@SPI注解。
@SPI
public interface Operation {
int operate(int num1, int num2);
}
2)寫兩個(gè)簡(jiǎn)單的實(shí)現(xiàn)
沿用之前的兩個(gè)實(shí)現(xiàn)類。
3)添加一個(gè)配置文件
新增配置文件放在dubbo目錄下。
目錄結(jié)構(gòu)
文件內(nèi)容
division=com.api.impl.DivisionOperation
plus=com.api.impl.PlusOperation
4)測(cè)試程序
public class DubboSpiTest {
@Test
public void testOperation() throws Exception {
ExtensionLoader 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ì)比來看,Dubbo的SPI可以根據(jù)配置的kv值來獲取。在沒有拆解源碼之前,考慮一下如何實(shí)現(xiàn)。
我可能會(huì)用雙層Map來實(shí)現(xiàn)緩存:第一層的key為接口的class對(duì)象,value為一個(gè)map;第二層的key為擴(kuò)展名(配置文件中的key),value為實(shí)現(xiàn)類的class。實(shí)現(xiàn)懶加載的方式,當(dāng)運(yùn)行方法的時(shí)候創(chuàng)建空map。在真正獲取時(shí)先從緩存中查找具體實(shí)現(xiàn)類的class對(duì)象,找得到就直接返回、找不到就根據(jù)配置文件加載并緩存。
Dubbo又是如何實(shí)現(xiàn)的呢?
2.2.1 getExtensionLoader方法首先來拆解getExtensionLoader方法。
這是一個(gè)靜態(tài)的工廠方法,要求傳入的類型必須為接口并且有SPI的注解,用map做了個(gè)緩存,key為接口的class對(duì)象,而value是 ExtensionLoader對(duì)象。
2.2.2 getExtension方法再來拆解ExtensionLoader的getExtension方法。
這段代碼也不復(fù)雜,如果傳入的參數(shù)為'true',則返回默認(rèn)的擴(kuò)展類實(shí)例;否則,從緩存中獲取實(shí)例,如果有就從緩存中獲取,沒有的話就新建。用map做緩存,緩存了holder對(duì)象,而holder對(duì)象中存放擴(kuò)展類。用volatile關(guān)鍵字和雙重檢查來應(yīng)對(duì)多線程創(chuàng)建問題,這也是單例模式的常用寫法。
2.2.3 createExtension方法重點(diǎn)分析createExtension方法。
這段代碼由幾部分組成:
第二個(gè)沒啥好說的,我們重點(diǎn)來分析一下1、3、4三個(gè)部分。
1)getExtensionClasses方法
老套路,從緩存獲取,沒有的話創(chuàng)建并加入緩存。這里緩存的是一個(gè)擴(kuò)展名和class的關(guān)系。這個(gè)擴(kuò)展名就是在配置文件中的key。創(chuàng)建之前,先緩存了一下接口的限定名。加載配置文件的路徑是以下這幾個(gè)。
2)loadDirectory方法
獲取配置文件路徑,獲取classLoader,并使用loadResource方法做進(jìn)一步處理。
3)loadResource方法
loadResource加載了配置文件,并解析了配置文件中的內(nèi)容。loadClass 方法操作了不同的緩存。
首先判斷是否有Adaptive注解,有的話緩存到cacheAdaptiveClass(緩存結(jié)構(gòu)為class);然后判斷是否wrapperclasses,是的話緩存到cacheWrapperClass中(緩存結(jié)構(gòu)為Set);如果以上都不是,這個(gè)類就是個(gè)普通的類,存儲(chǔ)class和名稱的映射關(guān)系到cacheNames里(緩存結(jié)構(gòu)為Map)。
基本上getExtensionClasses方法就分析完了,可以看出來,其實(shí)并不是很復(fù)雜。
2.2.4 IOC1)injectExtension方法
這個(gè)方法實(shí)現(xiàn)了依賴注入,即IOC。首先通過反射獲取到實(shí)例的方法;然后遍歷,獲取setter方法;接著從objectFactory中獲取依賴對(duì)象;最后通過反射調(diào)用setter方法注入依賴。
objectFactory的變量類型為AdaptiveExtensionFactory。
2)AdaptiveExtensionFactory
這個(gè)類里面有個(gè)ExtensionFactory的列表,用來存儲(chǔ)其他類型的 ExtensionFactory。Dubbo提供了兩種ExtensionFactory,一種是SpiExtensionFactory, 用于創(chuàng)建自適應(yīng)的擴(kuò)展;另一種是SpringExtesionFactory,用于從Spring的IOC容器中獲取擴(kuò)展。配置文件一個(gè)在dubbo-common模塊,一個(gè)在dubbo-config模塊。
配置文件
SpiExtensionFactory中的Spi方式前面已經(jīng)解析過了。
SpringExtesionFactory是從ApplicationContext中獲取對(duì)應(yīng)的實(shí)例。先根據(jù)名稱查找,找不到的話,再根據(jù)類型查找。
依賴注入的部分也拆解完畢,看看這次拆解的最后一部分代碼。
2.2.5 AOP創(chuàng)建wrapper對(duì)象的部分,wrapper對(duì)象是從哪里來的呢?還記得之前拆解的第一步么,loadClass方法中有幾個(gè)緩存,其中wrapperclasses就是緩存這些wrapper的class。
從代碼中可以看出,只要構(gòu)造方法里有且只有唯一參數(shù),同時(shí)此參數(shù)為當(dāng)前傳入的接口類型,即為wrapper class。
此處循環(huán)創(chuàng)建wrapper實(shí)例,首先將instance做為構(gòu)造函數(shù)的參數(shù),通過反射來創(chuàng)建wrapper對(duì)象,然后再向wrapper中注入依賴。
看到這里,可能會(huì)有人有疑問:為什么要?jiǎng)?chuàng)建一個(gè)wrapper對(duì)象?其實(shí)很簡(jiǎn)單,系統(tǒng)要在真正調(diào)用的前后干點(diǎn)別的事唄。這個(gè)就有點(diǎn)類似于spring的aop了。
三、總結(jié)本文簡(jiǎn)單介紹了JDK的SPI和Dubbo的SPI用法,分析了JDK的SPI源碼和Dubbo的SPI源碼。在拆解的過程中可以看出,Dubbo的源碼還是很值得一讀的。在實(shí)現(xiàn)方面考慮得很周全,不僅有對(duì)多線程的處理、多層緩存,也有IOC、AOP的過程。不過,Dubbo的SPI就這么簡(jiǎn)單么?當(dāng)然不是,這篇只拆解了擴(kuò)展類的加載過程,Dubbo的SPI中還有個(gè)很復(fù)雜的擴(kuò)展點(diǎn)-自適應(yīng)機(jī)制。