大家好,我是三友~~
今天來跟大家聊一聊JAVA、Spring、Dubbo三者SPI機(jī)制的原理和區(qū)別。
其實(shí)我之前寫過一篇類似的文章,但是這篇文章主要是剖析dubbo的SPI機(jī)制的源碼,中間只是簡單地介紹了一下Java、Spring的SPI機(jī)制,并沒有進(jìn)行深入,所以本篇就來深入聊一聊這三者的原理和區(qū)別。
什么是SPI
SPI全稱為Service Provider Interface,是一種動態(tài)替換發(fā)現(xiàn)的機(jī)制,一種解耦非常優(yōu)秀的思想,SPI可以很靈活的讓接口和實(shí)現(xiàn)分離,讓api提供者只提供接口,第三方來實(shí)現(xiàn),然后可以使用配置文件的方式來實(shí)現(xiàn)替換或者擴(kuò)展,在框架中比較常見,提高框架的可擴(kuò)展性。
簡單來說SPI是一種非常優(yōu)秀的設(shè)計(jì)思想,它的核心就是解耦、方便擴(kuò)展。
Java SPI機(jī)制--ServiceLoader
ServiceLoader是Java提供的一種簡單的SPI機(jī)制的實(shí)現(xiàn),Java的SPI實(shí)現(xiàn)約定了以下兩件事:
- 文件必須放在META-INF/services/目錄底下
- 文件名必須為接口的全限定名,內(nèi)容為接口實(shí)現(xiàn)的全限定名
這樣就能夠通過ServiceLoader加載到文件中接口的實(shí)現(xiàn)。
來個(gè)demo
第一步,需要一個(gè)接口以及他的實(shí)現(xiàn)類
public interface LoadBalance {
}
public class RandomLoadBalance implements LoadBalance{
}
第二步,在META-INF/services/目錄創(chuàng)建一個(gè)文件名LoadBalance全限定名的文件,文件內(nèi)容為RandomLoadBalance的全限定名
測試類:
public class ServiceLoaderDemo {
public static void main(String[] args) {
ServiceLoader<LoadBalance> loadBalanceServiceLoader = ServiceLoader.load(LoadBalance.class);
Iterator<LoadBalance> iterator = loadBalanceServiceLoader.iterator();
while (iterator.hasNext()) {
LoadBalance loadBalance = iterator.next();
System.out.println("獲取到負(fù)載均衡策略:" + loadBalance);
}
}
}
測試結(jié)果:
此時(shí)就成功獲取到了實(shí)現(xiàn)。
在實(shí)際的框架設(shè)計(jì)中,上面這段測試代碼其實(shí)是框架作者寫到框架內(nèi)部的,而對于框架的使用者來說,要想自定義LoadBalance實(shí)現(xiàn),嵌入到框架,僅僅只需要寫接口的實(shí)現(xiàn)和spi文件即可。
實(shí)現(xiàn)原理
如下是ServiceLoader中一段核心代碼
首先獲取一個(gè)fullName,其實(shí)就是META-INF/services/接口的全限定名
然后通過ClassLoader獲取到資源,其實(shí)就是接口的全限定名文件對應(yīng)的資源,然后交給parse方法解析資源
parse方法其實(shí)就是通過IO流讀取文件的內(nèi)容,這樣就可以獲取到接口的實(shí)現(xiàn)的全限定名
再后面其實(shí)就是通過反射實(shí)例化對象,這里就不展示了。
所以其實(shí)不難發(fā)現(xiàn)ServiceLoader實(shí)現(xiàn)原理比較簡單,總結(jié)起來就是通過IO流讀取META-INF/services/接口的全限定名文件的內(nèi)容,然后反射實(shí)例化對象。
優(yōu)缺點(diǎn)
由于Java的SPI機(jī)制實(shí)現(xiàn)的比較簡單,所以他也有一些缺點(diǎn)。
第一點(diǎn)就是浪費(fèi)資源,雖然例子中只有一個(gè)實(shí)現(xiàn)類,但是實(shí)際情況下可能會有很多實(shí)現(xiàn)類,而Java的SPI會一股腦全進(jìn)行實(shí)例化,但是這些實(shí)現(xiàn)了不一定都用得著,所以就會白白浪費(fèi)資源。
第二點(diǎn)就是無法對區(qū)分具體的實(shí)現(xiàn),也就是這么多實(shí)現(xiàn)類,到底該用哪個(gè)實(shí)現(xiàn)呢?如果要判斷具體使用哪個(gè),只能依靠接口本身的設(shè)計(jì),比如接口可以設(shè)計(jì)為一個(gè)策略接口,又或者接口可以設(shè)計(jì)帶有優(yōu)先級的,但是不論怎樣設(shè)計(jì),框架作者都得寫代碼進(jìn)行判斷。
所以總得來說就是ServiceLoader無法做到按需加載或者按需獲取某個(gè)具體的實(shí)現(xiàn)。
使用場景
雖然說ServiceLoader可能有些缺點(diǎn),但是還是有使用場景的,比如說:
- 不需要選擇具體的實(shí)現(xiàn),每個(gè)被加載的實(shí)現(xiàn)都需要被用到
- 雖然需要選擇具體的實(shí)現(xiàn),但是可以通過對接口的設(shè)計(jì)來解決
Spring SPI機(jī)制--SpringFactoriesLoader
Spring我們都不陌生,他也提供了一種SPI的實(shí)現(xiàn)SpringFactoriesLoader。
Spring的SPI機(jī)制的約定如下:
- 配置文件必須在META-INF/目錄下,文件名必須為spring.factories
- 文件內(nèi)容為鍵值對,一個(gè)鍵可以有多個(gè)值,只需要用逗號分割就行,同時(shí)鍵值都需要是類的全限定名,鍵和值可以沒有任何類與類之間的關(guān)系,當(dāng)然也可以有實(shí)現(xiàn)的關(guān)系。
所以也可以看出,Spring的SPI機(jī)制跟Java的不論是文件名還是內(nèi)容約定都不一樣。
來個(gè)demo
在META-INF/目錄下創(chuàng)建spring.factories文件,LoadBalance為鍵,RandomLoadBalance為值
測試:
public class SpringFactoriesLoaderDemo {
public static void main(String[] args) {
List<LoadBalance> loadBalances = SpringFactoriesLoader.loadFactories(LoadBalance.class, MyEnableAutoConfiguration.class.getClassLoader());
for (LoadBalance loadBalance : loadBalances) {
System.out.println("獲取到LoadBalance對象:" + loadBalance);
}
}
}
運(yùn)行結(jié)果:
成功獲取到了實(shí)現(xiàn)對象。
核心原理
如下是SpringFactoriesLoader中一段核心代碼
其實(shí)從這可以看出,跟Java實(shí)現(xiàn)的差不多,只不過讀的是META-INF/目錄下spring.factories文件內(nèi)容,然后解析出來鍵值對。
使用場景
Spring的SPI機(jī)制在內(nèi)部使用的非常多,尤其在SpringBoot中大量使用,SpringBoot啟動過程中很多擴(kuò)展點(diǎn)都是通過SPI機(jī)制來實(shí)現(xiàn)的,這里我舉兩個(gè)例子
1、自動裝配
在SpringBoot3.0之前的版本,自動裝配是通過SpringFactoriesLoader來加載的。
但是SpringBoot3.0之后不再使用SpringFactoriesLoader,而是Spring重新從META-INF/spring/目錄下的org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中讀取了。
至于如何讀取的,其實(shí)猜也能猜到就跟上面SPI機(jī)制讀取的方式大概差不多,就是文件路徑和名稱不一樣。
2、PropertySourceLoader的加載
PropertySourceLoader是用來解析Application配置文件的,它是一個(gè)接口
SpringBoot默認(rèn)提供了 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader兩個(gè)實(shí)現(xiàn),就是對應(yīng)properties和yaml文件格式的解析。
SpringBoot在加載PropertySourceLoader時(shí)就用了SPI機(jī)制
與Java SPI機(jī)制對比
首先Spring的SPI機(jī)制對Java的SPI機(jī)制對進(jìn)行了一些簡化,Java的SPI每個(gè)接口都需要對應(yīng)的文件,而Spring的SPI機(jī)制只需要一個(gè)spring.factories文件。
其次是內(nèi)容,Java的SPI機(jī)制文件內(nèi)容必須為接口的實(shí)現(xiàn)類,而Spring的SPI并不要求鍵值對必須有什么關(guān)系,更加靈活。
第三點(diǎn)就是Spring的SPI機(jī)制提供了獲取類限定名的方法loadFactoryNames,而Java的SPI機(jī)制是沒有的。通過這個(gè)方法獲取到類限定名之后就可以將這些類注入到Spring容器中,用Spring容器加載這些Bean,而不僅僅是通過反射。
但是Spring的SPI也同樣沒有實(shí)現(xiàn)獲取指定某個(gè)指定實(shí)現(xiàn)類的功能,所以要想能夠找到具體的某個(gè)實(shí)現(xiàn)類,還得依靠具體接口的設(shè)計(jì)。
所以不知道你有沒有發(fā)現(xiàn),PropertySourceLoader它其實(shí)就是一個(gè)策略接口,注釋也有說,所以當(dāng)你的配置文件是properties格式的時(shí)候,他可以找到解析properties格式的PropertiesPropertySourceLoader對象來解析配置文件。
Dubbo SPI機(jī)制--ExtensionLoader
ExtensionLoader是dubbo的SPI機(jī)制的實(shí)現(xiàn)類。每一個(gè)接口都會有一個(gè)自己的ExtensionLoader實(shí)例對象,這點(diǎn)跟Java的SPI機(jī)制是一樣的。
同樣地,Dubbo的SPI機(jī)制也做了以下幾點(diǎn)約定:
- 接口必須要加@SPI注解
- 配置文件可以放在META-INF/services/、META-INF/dubbo/internal/ 、META-INF/dubbo/ 、META-INF/dubbo/external/這四個(gè)目錄底下,文件名也是接口的全限定名
- 內(nèi)容為鍵值對,鍵為短名稱(可以理解為spring中Bean的名稱),值為實(shí)現(xiàn)類的全限定名
先來個(gè)demo
首先在LoadBalance接口上@SPI注解
@SPI
public interface LoadBalance {
}
然后,修改一下Java的SPI機(jī)制測試時(shí)配置文件內(nèi)容,改為鍵值對,因?yàn)镈ubbo的SPI機(jī)制也可以從META-INF/services/目錄下讀取文件,所以這里就沒重寫文件
random=com.sanyou.spi.demo.RandomLoadBalance
測試類:
public class ExtensionLoaderDemo {
public static void main(String[] args) {
ExtensionLoader<LoadBalance> extensionLoader = ExtensionLoader.getExtensionLoader(LoadBalance.class);
LoadBalance loadBalance = extensionLoader.getExtension("random");
System.out.println("獲取到random鍵對應(yīng)的實(shí)現(xiàn)類對象:" + loadBalance);
}
}
通過ExtensionLoader的getExtension方法,傳入短名稱,這樣就可以精確地找到短名稱對的實(shí)現(xiàn)類。
所以從這可以看出Dubbo的SPI機(jī)制解決了前面提到的無法獲取指定實(shí)現(xiàn)類的問題。
測試結(jié)果:
dubbo的SPI機(jī)制除了解決了無法獲取指定實(shí)現(xiàn)類的問題,還提供了很多額外的功能,這些功能在dubbo內(nèi)部用的非常多,接下來就來詳細(xì)講講。
dubbo核心機(jī)制
1、自適應(yīng)機(jī)制
自適應(yīng),自適應(yīng)擴(kuò)展類的含義是說,基于參數(shù),在運(yùn)行時(shí)動態(tài)選擇到具體的目標(biāo)類,然后執(zhí)行。
每個(gè)接口有且只能有一個(gè)自適應(yīng)類,通過ExtensionLoader的getAdaptiveExtension方法就可以獲取到這個(gè)類的對象,這個(gè)對象可以根據(jù)運(yùn)行時(shí)具體的參數(shù)找到目標(biāo)實(shí)現(xiàn)類對象,然后調(diào)用目標(biāo)對象的方法。
舉個(gè)例子,假設(shè)上面的LoadBalance有個(gè)自適應(yīng)對象,那么獲取到這個(gè)自適應(yīng)對象之后,如果在運(yùn)行期間傳入了random這個(gè)key,那么這個(gè)自適應(yīng)對象就會找到random這個(gè)key對應(yīng)的實(shí)現(xiàn)類,調(diào)用那個(gè)實(shí)現(xiàn)類的方法,如果動態(tài)傳入了其它的key,就路由到其它的實(shí)現(xiàn)類。
自適應(yīng)類有兩種方式產(chǎn)生,第一種就是自己指定,在接口的實(shí)現(xiàn)類上加@Adaptive注解,那么這個(gè)這個(gè)實(shí)現(xiàn)類就是自適應(yīng)實(shí)現(xiàn)類。
@Adaptive
public class RandomLoadBalance implements LoadBalance{
}
除了自己代碼指定,還有一種就是dubbo會根據(jù)一些條件幫你動態(tài)生成一個(gè)自適應(yīng)類,生成過程比較復(fù)雜,這里就不展開了。
自適應(yīng)機(jī)制在Dubbo中用的非常多,而且很多都是自動生成的,如果你不知道Dubbo的自適應(yīng)機(jī)制,你在讀源碼的時(shí)候可能都不知道為什么代碼可以走到那里。。
2、IOC和AOP
一提到IOC和AOP,立馬想到的都是Spring,但是IOC和AOP并不是Spring特有的概念,Dubbo也實(shí)現(xiàn)IOC和AOP的功能,但是是一個(gè)輕量級的。
2.1、依賴注入
Dubbo依賴注入是通過setter注入的方式,注入的對象默認(rèn)就是上面提到的自適應(yīng)的對象,在Spring環(huán)境下可以注入Spring Bean。
public class RoundRobinLoadBalance implements LoadBalance {
private LoadBalance loadBalance;
public void setLoadBalance(LoadBalance loadBalance) {
this.loadBalance = loadBalance;
}
}
如上代碼,RoundRobinLoadBalance中有一個(gè)setLoadBalance方法,參數(shù)LoadBalance,在創(chuàng)建RoundRobinLoadBalance的時(shí)候,在非Spring環(huán)境底下,Dubbo就會找到LoadBalance自適應(yīng)對象然后通過反射注入。
這種方式在Dubbo中也很常見,比如如下的一個(gè)場景
RegistryProtocol中會注入一個(gè)Protocol,其實(shí)這個(gè)注入的Protocol就是一個(gè)自適應(yīng)對象。
2.2、接口回調(diào)
Dubbo也提供了一些類似于Spring的一些接口的回調(diào)功能,比如說,如果你的類實(shí)現(xiàn)了Lifecycle接口,那么創(chuàng)建或者銷毀的時(shí)候就會回調(diào)以下幾個(gè)方法
在dubbo3.x的某個(gè)版本之后,dubbo提供了更多接口回調(diào),比如說ExtensionPostProcessor、ExtensionAccessorAware,命名跟Spring的非常相似,作用也差不多。
2.3、自動包裝
自動包裝其實(shí)就是aop的功能實(shí)現(xiàn),對目標(biāo)對象進(jìn)行代理,并且這個(gè)aop功能在默認(rèn)情況下就是開啟的。
在Dubbo中SPI接口的實(shí)現(xiàn)中,有一種特殊的類,被稱為Wrapper類,這個(gè)類的作用就是來實(shí)現(xiàn)AOP的。
判斷Wrapper類的唯一標(biāo)準(zhǔn)就是這個(gè)類中必須要有這么一個(gè)構(gòu)造參數(shù),這個(gè)構(gòu)造方法的參數(shù)只有一個(gè),并且參數(shù)類型就是接口的類型,如下代碼:
public class RoundRobinLoadBalance implements LoadBalance {
private final LoadBalance loadBalance;
public RoundRobinLoadBalance(LoadBalance loadBalance) {
this.loadBalance = loadBalance;
}
}
此時(shí)RoundRobinLoadBalance就是一個(gè)Wrapper類。
當(dāng)通過random獲取RandomLoadBalance目標(biāo)對象時(shí),那么默認(rèn)情況下就會對RandomLoadBalance進(jìn)行包裝,真正獲取到的其實(shí)是RoundRobinLoadBalance對象,RoundRobinLoadBalance內(nèi)部引用的對象是RandomLoadBalance。
測試一下
在配置文件中加入
roundrobin=com.sanyou.spi.demo.RoundRobinLoadBalance
測試結(jié)果
從結(jié)果可以看出,雖然指定了random,但是實(shí)際獲取到的是RoundRobinLoadBalance,而RoundRobinLoadBalance內(nèi)部引用了RandomLoadBalance。
如果有很多的包裝類,那么就會形成一個(gè)責(zé)任鏈條,一個(gè)套一個(gè)。
所以dubbo的aop跟spring的aop實(shí)現(xiàn)是不一樣的,spring的aop底層是基于動態(tài)代理來的,而dubbo的aop其實(shí)算是靜態(tài)代理,dubbo會幫你自動組裝這個(gè)代理,形成一條責(zé)任鏈。
到這其實(shí)我們已經(jīng)知道,dubbo的spi接口的實(shí)現(xiàn)類已經(jīng)有兩種類型了:
- 自適應(yīng)類
- Wrapper類
除了這兩種類型,其實(shí)還有一種,叫做默認(rèn)類,就是@SPI注解的值對應(yīng)的實(shí)現(xiàn)類,比如
@SPI("random")
public interface LoadBalance {
}
此時(shí)random這個(gè)key對應(yīng)的實(shí)現(xiàn)類就是默認(rèn)實(shí)現(xiàn),通過getDefaultExtension這個(gè)方法就可以獲取到默認(rèn)實(shí)現(xiàn)對象。
3、自動激活
所謂的自動激活,就是根據(jù)你的入?yún)ⅲ瑒討B(tài)地選擇一批實(shí)現(xiàn)類返回給你。
自動激活的實(shí)現(xiàn)類上需要加上Activate注解,這里就又學(xué)習(xí)了一種實(shí)現(xiàn)類的分類。
@Activate
public interface RandomLoadBalance {
}
此時(shí)RandomLoadBalance就屬于可以被自動激活的類。
獲取自動激活類的方法是getActivateExtension,所以根據(jù)這個(gè)方法的入?yún)ⅲ梢詣討B(tài)選擇一批實(shí)現(xiàn)類。
自動激活這個(gè)機(jī)制在Dubbo一個(gè)核心的使用場景就是Filter過濾器鏈中。
Filter是dubbo中的一個(gè)擴(kuò)展點(diǎn),可以在請求發(fā)起前或者是響應(yīng)獲取之后就行攔截,作用有點(diǎn)像Spring MVC中的HandlerInterceptor。
Filter的一些實(shí)現(xiàn)類
如上Filter有很多實(shí)現(xiàn),所以為了能夠區(qū)分Filter的實(shí)現(xiàn)是作用于provider的還是consumer端,所以就可以用自動激活的機(jī)制來根據(jù)入?yún)韯討B(tài)選擇一批Filter實(shí)現(xiàn)。
比如說ConsumerContextFilter這個(gè)Filter就作用于Consumer端。
ConsumerContextFilter
最后,這里并沒有對dubbo的SPI機(jī)制進(jìn)行源碼分析,感興趣的同學(xué)可以看一下面試常問的dubbo的spi機(jī)制到底是什么?(上)和 面試常問的dubbo的spi機(jī)制到底是什么?(下)兩篇文章。
總結(jié)
通過以上分析可以看出,實(shí)現(xiàn)SPI機(jī)制的核心原理就是通過IO流讀取指定文件的內(nèi)容,然后解析,最后加入一些自己的特性。
最后總的來說,Java的SPI實(shí)現(xiàn)的比較簡單,并沒有什么其它功能;Spring得益于自身的ioc和aop的功能,所以也沒有實(shí)現(xiàn)太復(fù)雜的SPI機(jī)制,僅僅是對Java做了一點(diǎn)簡化和優(yōu)化;但是dubbo的SPI機(jī)制為了滿足自身框架的使用要求,實(shí)現(xiàn)的功能就比較多,不僅將ioc和aop的功能集成到SPI機(jī)制中,還提供注入自適應(yīng)和自動激活等功能。