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