阿里面试真题:Dubbo的SPI机制
點(diǎn)贊再看,養(yǎng)成習(xí)慣,微信搜一搜【三太子敖丙】關(guān)注這個(gè)喜歡寫情懷的程序員。
本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點(diǎn)、資料以及我的系列文章。
前言
上一篇 Dubbo 文章敖丙已經(jīng)帶了大家過了一遍整體的架構(gòu),也提到了 Dubbo 的成功離不開它采用微內(nèi)核設(shè)計(jì)+SPI擴(kuò)展,使得有特殊需求的接入方可以自定義擴(kuò)展,做定制的二次開發(fā)。
良好的擴(kuò)展性對(duì)于一個(gè)框架而言尤其重要,框架顧名思義就是搭好核心架子,給予用戶簡(jiǎn)單便捷的使用,同時(shí)也需要滿足他們定制化的需求。
Dubbo 就依靠 SPI 機(jī)制實(shí)現(xiàn)了插件化功能,幾乎將所有的功能組件做成基于 SPI 實(shí)現(xiàn),并且默認(rèn)提供了很多可以直接使用的擴(kuò)展點(diǎn),實(shí)現(xiàn)了面向功能進(jìn)行拆分的對(duì)擴(kuò)展開放的架構(gòu)。
什么是 SPI
首先我們得先知道什么叫 SPI。
SPI (Service Provider Interface),主要是用來在框架中使用的,最常見和莫過于我們?cè)谠L問數(shù)據(jù)庫(kù)時(shí)候用到的java.sql.Driver接口了。
你想一下首先市面上的數(shù)據(jù)庫(kù)五花八門,不同的數(shù)據(jù)庫(kù)底層協(xié)議的大不相同,所以首先需要定制一個(gè)接口,來約束一下這些數(shù)據(jù)庫(kù),使得 Java 語(yǔ)言的使用者在調(diào)用數(shù)據(jù)庫(kù)的時(shí)候可以方便、統(tǒng)一的面向接口編程。
數(shù)據(jù)庫(kù)廠商們需要根據(jù)接口來開發(fā)他們對(duì)應(yīng)的實(shí)現(xiàn),那么問題來了,真正使用的時(shí)候到底用哪個(gè)實(shí)現(xiàn)呢?從哪里找到實(shí)現(xiàn)類呢?
這時(shí)候 Java SPI 機(jī)制就派上用場(chǎng)了,不知道到底用哪個(gè)實(shí)現(xiàn)類和找不到實(shí)現(xiàn)類,我們告訴它不就完事了唄。
大家都約定好將實(shí)現(xiàn)類的配置寫在一個(gè)地方,然后到時(shí)候都去哪個(gè)地方查一下不就知道了嗎?
Java SPI 就是這樣做的,約定在 Classpath 下的 META-INF/services/ 目錄里創(chuàng)建一個(gè)以服務(wù)接口命名的文件,然后文件里面記錄的是此 jar 包提供的具體實(shí)現(xiàn)類的全限定名。
這樣當(dāng)我們引用了某個(gè) jar 包的時(shí)候就可以去找這個(gè) jar 包的 META-INF/services/ 目錄,再根據(jù)接口名找到文件,然后讀取文件里面的內(nèi)容去進(jìn)行實(shí)現(xiàn)類的加載與實(shí)例化。
比如我們看下 MySQL 是怎么做的。
再來看一下文件里面的內(nèi)容。
MySQL 就是這樣做的,為了讓大家更加深刻的理解我再簡(jiǎn)單的寫一個(gè)示例。
Java SPI 示例
然后我在 META-INF/services/ 目錄下建了個(gè)以接口全限定名命名的文件,內(nèi)容如下
com.demo.spi.NuanNanAobing com.demo.spi.ShuaiAobing運(yùn)行之后的結(jié)果如下
Java SPI 源碼分析
之前的文章我也提到了 Dubbo 并沒有用 Java 實(shí)現(xiàn)的 SPI,而是自定義 SPI,那肯定是 Java SPI 有什么不方便的地方或者劣勢(shì)。
因此丙帶著大家先深入了解一下 Java SPI,這樣才能知道哪里不好,進(jìn)而再和 Dubbo SPI 進(jìn)行對(duì)比的時(shí)候會(huì)更加的清晰其優(yōu)勢(shì)。
大家看到源碼不要怕,丙已經(jīng)給大家做了注釋,并且邏輯也不難的,想要變強(qiáng)源碼不可或缺。為了讓大家更好的理解,丙在源碼分析完了之后還會(huì)畫個(gè)圖,幫大家再理一下思路。
從上面我的示例中可以看到ServiceLoader.load()其實(shí)就是 Java SPI 入口,我們來看看到底做了什么操作。
我用一句話概括一下,簡(jiǎn)單的說就是先找當(dāng)前線程綁定的 ClassLoader,如果沒有就用 SystemClassLoader,然后清除一下緩存,再創(chuàng)建一個(gè) LazyIterator。
那現(xiàn)在重點(diǎn)就是 LazyIterator了,從上面代碼可以看到我們調(diào)用了 hasNext() 來做實(shí)例循環(huán),通過 next() 得到一個(gè)實(shí)例。而 LazyIterator 其實(shí)就是 Iterator 的實(shí)現(xiàn)類。我們來看看它到底干了啥。
不管進(jìn)入 if 分支還是 else 分支,重點(diǎn)都在我框出來的代碼,接下來就進(jìn)入重要時(shí)刻了!
可以看到這個(gè)方法其實(shí)就是在約定好的地方找到接口對(duì)應(yīng)的文件,然后加載文件并且解析文件里面的內(nèi)容。
我們?cè)賮砜匆幌?nextService()。
所以就是通過文件里填寫的全限定名加載類,并且創(chuàng)建其實(shí)例放入緩存之后返回實(shí)例。
整體的 Java SPI 的源碼解析已經(jīng)完畢,是不是很簡(jiǎn)單?就是約定一個(gè)目錄,根據(jù)接口名去那個(gè)目錄找到文件,文件解析得到實(shí)現(xiàn)類的全限定名,然后循環(huán)加載實(shí)現(xiàn)類和創(chuàng)建其實(shí)例。
我再用一張圖來帶大家過一遍。
想一下 Java SPI 哪里不好
相信大家一眼就能看出來,Java SPI 在查找擴(kuò)展實(shí)現(xiàn)類的時(shí)候遍歷 SPI 的配置文件并且將實(shí)現(xiàn)類全部實(shí)例化,假設(shè)一個(gè)實(shí)現(xiàn)類初始化過程比較消耗資源且耗時(shí),但是你的代碼里面又用不上它,這就產(chǎn)生了資源的浪費(fèi)。
所以說 Java SPI 無(wú)法按需加載實(shí)現(xiàn)類。
Dubbo SPI
因此 Dubbo 就自己實(shí)現(xiàn)了一個(gè) SPI,讓我們想一下按需加載的話首先你得給個(gè)名字,通過名字去文件里面找到對(duì)應(yīng)的實(shí)現(xiàn)類全限定名然后加載實(shí)例化即可。
Dubbo 就是這樣設(shè)計(jì)的,配置文件里面存放的是鍵值對(duì),我截一個(gè) Cluster 的配置。
并且 Dubbo SPI 除了可以按需加載實(shí)現(xiàn)類之外,增加了 IOC 和 AOP 的特性,還有個(gè)自適應(yīng)擴(kuò)展機(jī)制。
我們先來看一下 Dubbo 對(duì)配置文件目錄的約定,不同于 Java SPI ,Dubbo 分為了三類目錄。
-
META-INF/services/ 目錄:該目錄下的 SPI 配置文件是為了用來兼容 Java SPI 。
-
META-INF/dubbo/ 目錄:該目錄存放用戶自定義的 SPI 配置文件。
-
META-INF/dubbo/internal/ 目錄:該目錄存放 Dubbo 內(nèi)部使用的 SPI 配置文件。
Dubbo SPI 簡(jiǎn)單實(shí)例
用法很是簡(jiǎn)單,我就拿官網(wǎng)上的例子來展示一下。
首先在 META-INF/dubbo 目錄下按接口全限定名建立一個(gè)文件,內(nèi)容如下:
optimusPrime = org.apache.spi.OptimusPrime bumblebee = org.apache.spi.Bumblebee然后在接口上標(biāo)注@SPI 注解,以表明它要用SPI機(jī)制,類似下面這個(gè)圖(我就是拿 Cluster 的圖舉個(gè)例子,和這個(gè)示例代碼定義的接口不一樣)。
接著通過下面的示例代碼即可加載指定的實(shí)現(xiàn)類。
再來看一下運(yùn)行的結(jié)果。
Dubbo 源碼分析
此次分析的源碼版本是 2.6.5
相信通過上面的描述大家已經(jīng)對(duì) Dubbo SPI 已經(jīng)有了一定的認(rèn)識(shí),接下來我們來看看它的實(shí)現(xiàn)。
從上面的示例代碼我們知道 ExtensionLoader 好像就是重點(diǎn),它是類似 Java SPI 中 ServiceLoader 的存在。
我們可以看到大致流程就是先通過接口類找到一個(gè) ExtensionLoader ,然后再通過 ExtensionLoader.getExtension(name) 得到指定名字的實(shí)現(xiàn)類實(shí)例。
我們就先看下 getExtensionLoader() 做了什么。
很簡(jiǎn)單,做了一些判斷然后從緩存里面找是否已經(jīng)存在這個(gè)類型的 ExtensionLoader ,如果沒有就新建一個(gè)塞入緩存。最后返回接口類對(duì)應(yīng)的 ExtensionLoader 。
我們?cè)賮砜匆幌?getExtension() 方法,從現(xiàn)象我們可以知道這個(gè)方法就是從類對(duì)應(yīng)的 ExtensionLoader 中通過名字找到實(shí)例化完的實(shí)現(xiàn)類。
可以看到重點(diǎn)就是 createExtension(),我們?cè)賮砜聪逻@個(gè)方法干了啥。
整體邏輯很清晰,先找實(shí)現(xiàn)類,判斷緩存是否有實(shí)例,沒有就反射建個(gè)實(shí)例,然后執(zhí)行 set 方法依賴注入。如果有找到包裝類的話,再包一層。
到這步為止我先畫個(gè)圖,大家理一理,還是很簡(jiǎn)單的。
那么問題來了 getExtensionClasses() 是怎么找的呢?injectExtension() 如何注入的呢(其實(shí)我已經(jīng)說了set方法注入)?為什么需要包裝類呢?
getExtensionClasses
這個(gè)方法進(jìn)去也是先去緩存中找,如果緩存是空的,那么調(diào)用 loadExtensionClasses,我們就來看下這個(gè)方法。
而 loadDirectory里面就是根據(jù)類名和指定的目錄,找到文件先獲取所有的資源,然后一個(gè)一個(gè)去加載類,然后再通過loadClass去做一下緩存操作。
可以看到,loadClass 之前已經(jīng)加載了類,loadClass 只是根據(jù)類上面的情況做不同的緩存。分別有 Adaptive 、WrapperClass 和普通類這三種,普通類又將Activate記錄了一下。至此對(duì)于普通的類來說整個(gè) SPI 過程完結(jié)了。
接下來我們分別看不是普通類的幾種東西是干啥用的。
Adaptive 注解 - 自適應(yīng)擴(kuò)展
在進(jìn)入這個(gè)注解分析之前,我們需要知道 Dubbo 的自適應(yīng)擴(kuò)展機(jī)制。
我們先來看一個(gè)場(chǎng)景,首先我們根據(jù)配置來進(jìn)行 SPI 擴(kuò)展的加載,但是我不想在啟動(dòng)的時(shí)候讓擴(kuò)展被加載,我想根據(jù)請(qǐng)求時(shí)候的參數(shù)來動(dòng)態(tài)選擇對(duì)應(yīng)的擴(kuò)展。
怎么做呢?
Dubbo 通過一個(gè)代理機(jī)制實(shí)現(xiàn)了自適應(yīng)擴(kuò)展,簡(jiǎn)單的說就是為你想擴(kuò)展的接口生成一個(gè)代理類,可以通過JDK 或者 javassist 編譯你生成的代理類代碼,然后通過反射創(chuàng)建實(shí)例。
這個(gè)實(shí)例里面的實(shí)現(xiàn)會(huì)根據(jù)本來方法的請(qǐng)求參數(shù)得知需要的擴(kuò)展類,然后通過 ExtensionLoader.getExtensionLoader(type.class).getExtension(從參數(shù)得來的name),來獲取真正的實(shí)例來調(diào)用。
我從官網(wǎng)搞了個(gè)例子,大家來看下。
現(xiàn)在大家應(yīng)該對(duì)自適應(yīng)擴(kuò)展有了一定的認(rèn)識(shí)了,我們?cè)賮砜聪略创a,到底怎么做的。
這個(gè)注解就是自適應(yīng)擴(kuò)展相關(guān)的注解,可以修飾類和方法上,在修飾類的時(shí)候不會(huì)生成代理類,因?yàn)檫@個(gè)類就是代理類,修飾在方法上的時(shí)候會(huì)生成代理類。
Adaptive 注解在類上
比如這個(gè) ExtensionFactory 有三個(gè)實(shí)現(xiàn)類,其中一個(gè)實(shí)現(xiàn)類就被標(biāo)注了 Adaptive 注解。
在 ExtensionLoader 構(gòu)造的時(shí)候就會(huì)去通過getAdaptiveExtension 獲取指定的擴(kuò)展類的 ExtensionFactory。
我們?cè)賮砜聪?AdaptiveExtensionFactory 的實(shí)現(xiàn)。
可以看到先緩存了所有實(shí)現(xiàn)類,然后在獲取的時(shí)候通過遍歷找到對(duì)應(yīng)的 Extension。
我們?cè)賮砩钊敕治鲆徊?getAdaptiveExtension 里面到底干了什么。
到這里其實(shí)已經(jīng)和上文分析的 getExtensionClasses中l(wèi)oadClass 對(duì) Adaptive 特殊緩存相呼應(yīng)上了。
Adaptive 注解在方法上
注解在方法上則需要?jiǎng)討B(tài)拼接代碼,然后動(dòng)態(tài)生成類,我們以 Protocol 為例子來看一下。
Protocol 沒有實(shí)現(xiàn)類注釋了 Adaptive ,但是接口上有兩個(gè)方法注解了 Adaptive ,有兩個(gè)方法沒有。
因此它走的邏輯應(yīng)該應(yīng)該是 createAdaptiveExtensionClass,
具體在里面如何生成代碼的我就不再深入了,有興趣的自己去看吧,我就把成品解析一下,就差不多了。
我美化一下給大家看看。
可以看到會(huì)生成包,也會(huì)生成 import 語(yǔ)句,類名就是接口加個(gè)$Adaptive,并且實(shí)現(xiàn)這接口,沒有標(biāo)記 Adaptive 注解的方法調(diào)用的話直接拋錯(cuò)。
我們?cè)賮砜匆幌聵?biāo)注了注解的方法,我就拿 export 舉例。
就像我前面說的那樣,根據(jù)請(qǐng)求的參數(shù),即 URL 得到具體要調(diào)用的實(shí)現(xiàn)類名,然后再調(diào)用 getExtension 獲取。
整個(gè)自適應(yīng)擴(kuò)展流程如下。
WrapperClass - AOP
包裝類是因?yàn)橐粋€(gè)擴(kuò)展接口可能有多個(gè)擴(kuò)展實(shí)現(xiàn)類,而這些擴(kuò)展實(shí)現(xiàn)類會(huì)有一個(gè)相同的或者公共的邏輯,如果每個(gè)實(shí)現(xiàn)類都寫一遍代碼就重復(fù)了,并且比較不好維護(hù)。
因此就搞了個(gè)包裝類,Dubbo 里幫你自動(dòng)包裝,只需要某個(gè)擴(kuò)展類的構(gòu)造函數(shù)只有一個(gè)參數(shù),并且是擴(kuò)展接口類型,就會(huì)被判定為包裝類,然后記錄下來,用來包裝別的實(shí)現(xiàn)類。
簡(jiǎn)單又巧妙,這就是 AOP 了。
injectExtension - IOC
直接看代碼,很簡(jiǎn)單,就是查找 set 方法,根據(jù)參數(shù)找到依賴對(duì)象則注入。
這就是 IOC。
Activate 注解
這個(gè)注解我就簡(jiǎn)單的說下,拿 Filter 舉例,Filter 有很多實(shí)現(xiàn)類,在某些場(chǎng)景下需要其中的幾個(gè)實(shí)現(xiàn)類,而某些場(chǎng)景下需要另外幾個(gè),而 Activate 注解就是標(biāo)記這個(gè)用的。
它有三個(gè)屬性,group 表示修飾在哪個(gè)端,是 provider 還是 consumer,value 表示在 URL參數(shù)中出現(xiàn)才會(huì)被激活,order 表示實(shí)現(xiàn)類的順序。
總結(jié)
先放個(gè)上述過程完整的圖。
然后我們?cè)賮砜偨Y(jié)一下,今天丙先帶大家了解了下什么是 SPI,寫了個(gè)簡(jiǎn)單示例,并且進(jìn)行了 Java SPI 源碼分析。
得知了 Java SPI 會(huì)一次加載和實(shí)例化所有的實(shí)現(xiàn)類。
而 Dubbo SPI 則自己實(shí)現(xiàn)了 SPI,可以通過名字實(shí)例化指定的實(shí)現(xiàn)類,并且實(shí)現(xiàn)了 IOC 、AOP 與 自適應(yīng)擴(kuò)展 SPI 。
整體而言不是很難,也不會(huì)很繞,大家看了文章之后如果自己再過一遍收獲會(huì)更大。
絮叨
另外,敖丙把自己的面試文章整理成了一本電子書,共 1630頁(yè)!目錄如下,還有我復(fù)習(xí)時(shí)總結(jié)的面試題以及簡(jiǎn)歷模板
現(xiàn)在免費(fèi)送給大家,點(diǎn)贊后在我的公眾號(hào)三太子敖丙回復(fù) 【資料】 即可獲取。
我是敖丙,你知道的越多,你不知道的越多,我們下期見!
人才們的 【三連】 就是敖丙創(chuàng)作的最大動(dòng)力,如果本篇博客有任何錯(cuò)誤和建議,歡迎人才們留言!
文章持續(xù)更新,可以微信搜一搜「 三太子敖丙 」第一時(shí)間閱讀,回復(fù)【資料】有我準(zhǔn)備的一線大廠面試資料和簡(jiǎn)歷模板,本文 GitHub https://github.com/JavaFamily 已經(jīng)收錄,有大廠面試完整考點(diǎn),歡迎Star。
總結(jié)
以上是生活随笔為你收集整理的阿里面试真题:Dubbo的SPI机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java单元测试使用mock【转载】
- 下一篇: 微信小程序开发者工具上请求能成功,手机预