大家好,我是三友~~
今天來跟大家聊一聊Java、Spring、Dubbo三者SPI機制的原理和區別。
其實我之前寫過一篇類似的文章,但是這篇文章主要是剖析dubbo的SPI機制的原始碼,中間只是簡單地介紹了一下Java、Spring的SPI機制,並沒有進行深入,所以本篇就來深入聊一聊這三者的原理和區別。
SPI全稱為Service Provider Interface,是一種動態替換髮現的機制,一種解耦非常優秀的思想,SPI可以很靈活的讓介面和實現分離,讓api提供者只提供介面,第三方來實現,然後可以使用組態檔的方式來實現替換或者擴充套件,在框架中比較常見,提高框架的可延伸性。
簡單來說SPI是一種非常優秀的設計思想,它的核心就是解耦、方便擴充套件。
ServiceLoader是Java提供的一種簡單的SPI機制的實現,Java的SPI實現約定了以下兩件事:
META-INF/services/
目錄底下這樣就能夠通過ServiceLoader載入到檔案中介面的實現。
第一步,需要一個介面以及他的實現類
public interface LoadBalance {
}
public class RandomLoadBalance implements LoadBalance{
}
第二步,在META-INF/services/
目錄建立一個檔名LoadBalance全限定名的檔案,檔案內容為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("獲取到負載均衡策略:" + loadBalance);
}
}
}
測試結果:
此時就成功獲取到了實現。
在實際的框架設計中,上面這段測試程式碼其實是框架作者寫到框架內部的,而對於框架的使用者來說,要想自定義LoadBalance實現,嵌入到框架,僅僅只需要寫介面的實現和spi檔案即可。
如下是ServiceLoader中一段核心程式碼
首先獲取一個fullName,其實就是META-INF/services/介面的全限定名
然後通過ClassLoader獲取到資源,其實就是介面的全限定名檔案對應的資源,然後交給parse
方法解析資源
parse
方法其實就是通過IO流讀取檔案的內容,這樣就可以獲取到介面的實現的全限定名
再後面其實就是通過反射範例化物件,這裡就不展示了。
所以其實不難發現ServiceLoader實現原理比較簡單,總結起來就是通過IO流讀取META-INF/services/介面的全限定名
檔案的內容,然後反射範例化物件。
由於Java的SPI機制實現的比較簡單,所以他也有一些缺點。
第一點就是浪費資源,雖然例子中只有一個實現類,但是實際情況下可能會有很多實現類,而Java的SPI會一股腦全進行範例化,但是這些實現了不一定都用得著,所以就會白白浪費資源。
第二點就是無法對區分具體的實現,也就是這麼多實現類,到底該用哪個實現呢?如果要判斷具體使用哪個,只能依靠介面本身的設計,比如介面可以設計為一個策略介面,又或者介面可以設計帶有優先順序的,但是不論怎樣設計,框架作者都得寫程式碼進行判斷。
所以總得來說就是ServiceLoader無法做到按需載入或者按需獲取某個具體的實現。
雖然說ServiceLoader可能有些缺點,但是還是有使用場景的,比如說:
Spring我們都不陌生,他也提供了一種SPI的實現SpringFactoriesLoader。
Spring的SPI機制的約定如下:
META-INF/
目錄下,檔名必須為spring.factories所以也可以看出,Spring的SPI機制跟Java的不論是檔名還是內容約定都不一樣。
在META-INF/
目錄下建立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);
}
}
}
執行結果:
成功獲取到了實現物件。
如下是SpringFactoriesLoader中一段核心程式碼
其實從這可以看出,跟Java實現的差不多,只不過讀的是META-INF/
目錄下spring.factories檔案內容,然後解析出來鍵值對。
Spring的SPI機制在內部使用的非常多,尤其在SpringBoot中大量使用,SpringBoot啟動過程中很多擴充套件點都是通過SPI機制來實現的,這裡我舉兩個例子
在SpringBoot3.0之前的版本,自動裝配是通過SpringFactoriesLoader來載入的。
但是SpringBoot3.0之後不再使用SpringFactoriesLoader,而是Spring重新從META-INF/spring/
目錄下的org.springframework.boot.autoconfigure.AutoConfiguration.imports
檔案中讀取了。
至於如何讀取的,其實猜也能猜到就跟上面SPI機制讀取的方式大概差不多,就是檔案路徑和名稱不一樣。
PropertySourceLoader是用來解析application組態檔的,它是一個介面
SpringBoot預設提供了 PropertiesPropertySourceLoader 和 YamlPropertySourceLoader兩個實現,就是對應properties和yaml檔案格式的解析。
SpringBoot在載入PropertySourceLoader時就用了SPI機制
首先Spring的SPI機制對Java的SPI機制對進行了一些簡化,Java的SPI每個介面都需要對應的檔案,而Spring的SPI機制只需要一個spring.factories檔案。
其次是內容,Java的SPI機制檔案內容必須為介面的實現類,而Spring的SPI並不要求鍵值對必須有什麼關係,更加靈活。
第三點就是Spring的SPI機制提供了獲取類限定名的方法loadFactoryNames
,而Java的SPI機制是沒有的。通過這個方法獲取到類限定名之後就可以將這些類注入到Spring容器中,用Spring容器載入這些Bean,而不僅僅是通過反射。
但是Spring的SPI也同樣沒有實現獲取指定某個指定實現類的功能,所以要想能夠找到具體的某個實現類,還得依靠具體介面的設計。
所以不知道你有沒有發現,PropertySourceLoader它其實就是一個策略介面,註釋也有說,所以當你的組態檔是properties格式的時候,他可以找到解析properties格式的PropertiesPropertySourceLoader物件來解析組態檔。
ExtensionLoader是dubbo的SPI機制的實現類。每一個介面都會有一個自己的ExtensionLoader範例物件,這點跟Java的SPI機制是一樣的。
同樣地,Dubbo的SPI機制也做了以下幾點約定:
META-INF/services/
、META-INF/dubbo/internal/
、META-INF/dubbo/
、META-INF/dubbo/external/
這四個目錄底下,檔名也是介面的全限定名首先在LoadBalance介面上@SPI註解
@SPI
public interface LoadBalance {
}
然後,修改一下Java的SPI機制測試時組態檔內容,改為鍵值對,因為Dubbo的SPI機制也可以從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鍵對應的實現類物件:" + loadBalance);
}
}
通過ExtensionLoader的getExtension
方法,傳入短名稱,這樣就可以精確地找到短名稱對的實現類。
所以從這可以看出Dubbo的SPI機制解決了前面提到的無法獲取指定實現類的問題。
測試結果:
dubbo的SPI機制除了解決了無法獲取指定實現類的問題,還提供了很多額外的功能,這些功能在dubbo內部用的非常多,接下來就來詳細講講。
自適應,自適應擴充套件類的含義是說,基於引數,在執行時動態選擇到具體的目標類,然後執行。
每個介面有且只能有一個自適應類,通過ExtensionLoader的getAdaptiveExtension
方法就可以獲取到這個類的物件,這個物件可以根據執行時具體的引數找到目標實現類物件,然後呼叫目標物件的方法。
舉個例子,假設上面的LoadBalance有個自適應物件,那麼獲取到這個自適應物件之後,如果在執行期間傳入了random
這個key,那麼這個自適應物件就會找到random
這個key對應的實現類,呼叫那個實現類的方法,如果動態傳入了其它的key,就路由到其它的實現類。
自適應類有兩種方式產生,第一種就是自己指定,在介面的實現類上加@Adaptive註解,那麼這個這個實現類就是自適應實現類。
@Adaptive
public class RandomLoadBalance implements LoadBalance{
}
除了自己程式碼指定,還有一種就是dubbo會根據一些條件幫你動態生成一個自適應類,生成過程比較複雜,這裡就不展開了。
自適應機制在Dubbo中用的非常多,而且很多都是自動生成的,如果你不知道Dubbo的自適應機制,你在讀原始碼的時候可能都不知道為什麼程式碼可以走到那裡。。
一提到IOC和AOP,立馬想到的都是Spring,但是IOC和AOP並不是Spring特有的概念,Dubbo也實現IOC和AOP的功能,但是是一個輕量級的。
2.1、依賴注入
Dubbo依賴注入是通過setter注入的方式,注入的物件預設就是上面提到的自適應的物件,在Spring環境下可以注入Spring Bean。
public class RoundRobinLoadBalance implements LoadBalance {
private LoadBalance loadBalance;
public void setLoadBalance(LoadBalance loadBalance) {
this.loadBalance = loadBalance;
}
}
如上程式碼,RoundRobinLoadBalance中有一個setLoadBalance方法,引數LoadBalance,在建立RoundRobinLoadBalance的時候,在非Spring環境底下,Dubbo就會找到LoadBalance自適應物件然後通過反射注入。
這種方式在Dubbo中也很常見,比如如下的一個場景
RegistryProtocol中會注入一個Protocol,其實這個注入的Protocol就是一個自適應物件。
2.2、介面回撥
Dubbo也提供了一些類似於Spring的一些介面的回撥功能,比如說,如果你的類實現了Lifecycle介面,那麼建立或者銷燬的時候就會回撥以下幾個方法
在dubbo3.x的某個版本之後,dubbo提供了更多介面回撥,比如說ExtensionPostProcessor、ExtensionAccessorAware,命名跟Spring的非常相似,作用也差不多。
2.3、自動包裝
自動包裝其實就是aop的功能實現,對目標物件進行代理,並且這個aop功能在預設情況下就是開啟的。
在Dubbo中SPI介面的實現中,有一種特殊的類,被稱為Wrapper類,這個類的作用就是來實現AOP的。
判斷Wrapper類的唯一標準就是這個類中必須要有這麼一個構造引數,這個構造方法的引數只有一個,並且引數型別就是介面的型別,如下程式碼:
public class RoundRobinLoadBalance implements LoadBalance {
private final LoadBalance loadBalance;
public RoundRobinLoadBalance(LoadBalance loadBalance) {
this.loadBalance = loadBalance;
}
}
此時RoundRobinLoadBalance就是一個Wrapper類。
當通過random
獲取RandomLoadBalance目標物件時,那麼預設情況下就會對RandomLoadBalance進行包裝,真正獲取到的其實是RoundRobinLoadBalance物件,RoundRobinLoadBalance內部參照的物件是RandomLoadBalance。
測試一下
在組態檔中加入
roundrobin=com.sanyou.spi.demo.RoundRobinLoadBalance
測試結果
從結果可以看出,雖然指定了random
,但是實際獲取到的是RoundRobinLoadBalance,而RoundRobinLoadBalance內部參照了RandomLoadBalance。
如果有很多的包裝類,那麼就會形成一個責任鏈條,一個套一個。
所以dubbo的aop跟spring的aop實現是不一樣的,spring的aop底層是基於動態代理來的,而dubbo的aop其實算是靜態代理,dubbo會幫你自動組裝這個代理,形成一條責任鏈。
到這其實我們已經知道,dubbo的spi介面的實現類已經有兩種型別了:
除了這兩種型別,其實還有一種,叫做預設類,就是@SPI
註解的值對應的實現類,比如
@SPI("random")
public interface LoadBalance {
}
此時random
這個key對應的實現類就是預設實現,通過getDefaultExtension
這個方法就可以獲取到預設實現物件。
所謂的自動啟用,就是根據你的入參,動態地選擇一批實現類返回給你。
自動啟用的實現類上需要加上Activate
註解,這裡就又學習了一種實現類的分類。
@Activate
public interface RandomLoadBalance {
}
此時RandomLoadBalance就屬於可以被自動啟用的類。
獲取自動啟用類的方法是getActivateExtension
,所以根據這個方法的入參,可以動態選擇一批實現類。
自動啟用這個機制在Dubbo一個核心的使用場景就是Filter過濾器鏈中。
Filter是dubbo中的一個擴充套件點,可以在請求發起前或者是響應獲取之後就行攔截,作用有點像Spring MVC中的HandlerInterceptor。
如上Filter有很多實現,所以為了能夠區分Filter的實現是作用於provider的還是consumer端,所以就可以用自動啟用的機制來根據入參來動態選擇一批Filter實現。
比如說ConsumerContextFilter這個Filter就作用於Consumer端。
最後,這裡並沒有對dubbo的SPI機制進行原始碼分析,感興趣的同學可以看一下面試常問的dubbo的spi機制到底是什麼?(上)和 面試常問的dubbo的spi機制到底是什麼?(下)兩篇文章。
通過以上分析可以看出,實現SPI機制的核心原理就是通過IO流讀取指定檔案的內容,然後解析,最後加入一些自己的特性。
最後總的來說,Java的SPI實現的比較簡單,並沒有什麼其它功能;Spring得益於自身的ioc和aop的功能,所以也沒有實現太複雜的SPI機制,僅僅是對Java做了一點簡化和優化;但是dubbo的SPI機制為了滿足自身框架的使用要求,實現的功能就比較多,不僅將ioc和aop的功能整合到SPI機制中,還提供注入自適應和自動啟用等功能。
最後的最後,安利一下大鵬的《保你平安》這部電影,週末去看了,從整體來看我感覺還可以,電影中融入了一些喜劇、動作、恐怖等元素,雖然有些地方強行煽情,但是大鵬想要表達的整體思想還是很值得一看滴。
掃碼或者搜尋關注公眾號 三友的java日記 ,及時乾貨不錯過,公眾號致力於通過畫圖加上通俗易懂的語言講解技術,讓技術更加容易學習,回覆 面試 即可獲得一套面試真題。