面試常問的dubbo的spi機制到底是什麼?

三友的java日記發表於2022-06-07

 前言

dubbo是一款微服務開發框架,它提供了 RPC通訊 與 微服務治理 兩大關鍵能力。作為spring cloud alibaba體系中重要的一部分,隨著spring cloud alibaba在國內活躍起來,dubbo也越來越深受各大公司的青睞。本文就來對dubbo的spi機制原始碼進行剖析,看一看dubbo的spi到底有哪些特性和功能。 

一、什麼是spi機制? 

SPI (Service Provider Interface),主要用於擴充套件的作用。舉個例子來說,假如有一個框架有一個介面,他有自己預設的實現類,但是在程式碼執行的過程中,你不想用他的實現類或者想擴充套件一下他的實現類的功能,但是此時你又不能修改別人的原始碼,那麼此時該怎麼辦?這時spi機制就有了用武之地。一般框架的作者在設計這種介面的時候不會直接去new這個介面的實現類,而是在Classpath路徑底下將這個介面的實現類按作者約定的格式寫在一個配置檔案上,然後在執行的過程中通過java提供的api,從所有jar包中讀取所有的這個指定檔案中的內容,獲取到實現類,用這個實現類,這樣,如果你想自己替換原有的框架的實現,你就可以按照作者規定的方式配置實現,這樣就能使用你自己寫的實現類了。

spi機制其實體現了設計思想中的解耦思想,方便開發者對框架功能進行擴充套件。

二、java的spi機制 -- ServiceLoader 

java中最常見的spi機制應用就是資料庫驅動的載入,java其實就是定義了java語言跟資料庫互動的介面,但是具體的實現得交給各大資料庫廠商來實現,那麼java怎麼知道你的資料庫廠商的實現了?這時就需要spi機制了,java好約了定在 Classpath 路徑下的 META-INF/services/ 目錄裡建立一個以服務介面命名的檔案,然後內容是該資料庫廠商的實現的介面的全限定名,這樣資料庫廠商只要按照這個規則去配置,java就能找到。

我以mysql來舉例,看一下mysql是怎麼實現的。

 內容

  

java是通過ServiceLoader類來實現讀取配置檔案中的實現類的。大家有興趣可以看一下里面的程式碼,其實就是讀取到每個jar包底下的檔案,讀取裡面的內容。

三、spring中的spi機制 -- SpringFactoriesLoader

相信spring大家都不陌生,在spring擴充套件也是依賴spi機制完成的,只不過spring對於擴充套件檔案約定在Classpath 路徑下的 META-INF目錄底下,所有的檔名都是叫spring.factories,檔案裡的內容是一個以一個個鍵值對的方式儲存的,鍵為類的全限定名,值也為類的全限定名,如果有多個值,可以用逗號分割,有一點得注意的是,鍵和值本身約定並沒有類與類之間的依賴關係(當然也可以有,得看使用場景的約定),也就是說鍵值可以沒有任何關聯,鍵僅僅是一種標識,代表一種場景,最常見的自動裝配的註解,@EnableAutoConfiguration,也就是代表自動裝配的場景,當你需要你的類被自動裝配,就可以以這個註解的許可權定名鍵,你的類為名,這樣springboot在進行自動裝配的時候,就會拿這個鍵,找到你寫的實現類來完成自動裝配。

這裡我貼出了自動裝配時載入類的原始碼。

  

這裡其實就是通過@EnableAutoConfiguration的全限定名從spring.factories中載入這個鍵對應的所有的實現類的名稱,這樣就能拿到所有需要自動裝配的類的全限定名了。

mybatis整合spring的自動裝配功能檔案

圖片

內容

圖片

mybatis也是按照spring的規則來配置的。大家有空可以去看MybatisAutoConfiguration這個實現類,裡面有mybatis是如何跟spring整合的內容。

SpringFactoriesLoader的應用場景還有很多,大家可以去看一下SpringBoot中的啟動引導類SpringApplication,裡面多次使用到了這個SpringFactoriesLoader這個類來獲取各種實現。

四、dubbo的spi機制 -- ExtensionLoader原始碼剖析

本文是基於dubbo3.0.4版本原始碼剖析。 

講完了java和spring的中的spi機制,接下來進入本文的主題,dubbo的spi機制到底是什麼?它與java自帶的有何區別?為什麼不用java的spi機制?

ExtensionLoader是dubbo的spi機制所實現的類,通過這個類來載入介面所有實現類,獲取實現類的物件。同時每一個介面都會有一個自己的ExtensionLoader。

1)java的spi機制的缺點?

從我們分析java的spi機制可以看出,java約定了檔名為介面的名稱,內容為實現。不知道大家有沒有想過這裡面有個很嚴重的問題,就是雖然我獲取到了所有的實現類,但是無法對實現類進行分類,也就是說我無法確定到底該用哪個實現類,並且java的spi機制會一次性給所有的實現類建立物件,如果這個物件你根本不會使用,那麼此時就會白白浪費資源,也就是說無法做到按需載入。 

所以,dubbo就自己實現了一套spi機制,不僅解決了以上的痛點,同時也加入了更多的特性。

2)dubbo的配置檔案約束。

dubbo會從四個目錄讀取檔案META-INF/dubbo/internal/ 、META-INF/dubbo/ 、META-INF/services/、META-INF/dubbo/external/,檔名為介面的全限定名,內容為鍵值對,鍵為短名稱(可以理解為spring中的物件的名稱),值為實現類。

3)@SPI 註解的約束

dubbo中所有的擴充套件介面,都需要在介面上加@SPI註解,不然在建立ExtensionLoader的時候,會報錯。程式碼體現在這裡 

順便說說ExtensionDirector的作用,在3.0.3以前的版本,是沒有這個類的,但是在之後的版本為了實現一些新的特性,就抽象出來了這個類,通過這個類來獲取每個介面對應的ExtensionLoader

4)實現類的載入 

先說各種特性之前,先說一下這些實現類是如何載入的,類的載入是非常重要的一個環節,與後面的spi特性有重要的關係。

類載入預設都是先呼叫getExtensionClasses這個方法的,當cachedClasses沒有的時,才會去載入實現類,然後再把實現類放到cachedClasses中。真正實現載入的是loadExtensionClasses 方法,接下來我們詳細看這個方法的原始碼。

圖片

checkDestroyed();

方法沒什麼東西,其實就是一個檢查的作用。

cacheDefaultExtensionName();

快取預設實現類的短名稱。其實很簡單,就是從@SPI註解中取出名稱,就是預設的實現類的名稱,快取起來,ExtensionLoader有個getDefaultExtension方法,其實就是通過這個短名稱對應的實現類的物件。

接下來會遍歷LoadingStrategy,根據LoadingStrategy載入指定目錄的檔案。

我們先來看看LoadingStrategy的例項是怎麼載入的。我們進入loadLoadingStrategies方法, 

圖片

驚訝的發現竟然是使用了java的spi機制載入LoadingStrategy,那我們就去Classpath 路徑下的 META-INF/services/路徑下找這個LoadingStrategy介面的全限定名的檔案,看看有哪些實現。有四個實現,也就是會按照這四個的載入策略來讀取實現類。其中有個方法directory,就是指定載入的目錄,這也就是我們前面說的那幾個dubbo會載入的目錄,其實是從這個方法返回的,你可以自己去看看這四個實現類對於這個方法的實現。其實我們也可以實現這個介面,指定我們自己想載入的目錄。

這裡會迴圈載入每個目錄,我們進去loadDirectory方法。

這個其實就是拿出LoadingStrategy來呼叫過載的loadDirectory方法。

這裡注意會呼叫兩次loadDirectory,下面的那個其實是適配以前老版本的,不用關心。

接下來進去過載的loadDirectory方法。

可以看出,fileName就是LoadingStrategy所指定的目錄 +  介面的全限定名,這裡就解釋了為什麼實現類需要寫在類全限定名的檔案裡。其實就是從每個jar底指定的目錄類全限定名為名稱的檔案,得到每個jar底下的檔案的URL。然後遍歷每個URL,載入類,我們進入loadResource方法來看看具體是怎麼載入的。

圖片

通過URL開啟一個輸入流,然後讀取檔案內容,取出每一行,以 = 進行分割(因為規定的是以鍵值對存的),鍵就是短名稱,值就是實現類的名稱,然後再進入loadClass方法,這個方法很重要,其實是對實現類進行一個分類,後面dubbo的特性實現的前提就是對這些實現類的分類操作。

圖片

 

圖片

標紅的兩處是這個意思

 

如果你加了@Adaptive註解,那麼就將賦值到cachedAdaptiveClass屬性上。我們叫這個類為自適應類。什麼是自適應,其實說白了這個類本身並沒有實際的意義,它是根據你的入參動態來實現找到真正的實現類來完成呼叫。getAdaptiveExtension其實就是獲取到這個自適應實現類對應的物件。

 

如果你的實現類是有一個該型別為引數的構造方法,那麼就將這個實現類放到cachedWrapperClasses中,並且我們稱這個類為包裝類,什麼叫包裝,其實跟靜態代理有點像,就是將目標物件進行代理,可以增強功能。

圖片

這處標紅的意思是判斷是不是實現類是不是加了@Activate註解,是的話就將短名稱和註解放入cachedActivates中,我們稱這類實現類為自動啟用的類,所謂的自動啟用,就是可以根據你的入參,動態選擇實現一批符合條件的實現類

圖片

 saveInExtensionClass就是將這個實現類放入extensionClasses中,該目錄下的實現類就載入完成了。

接下來會繼續迴圈,載入不同的目錄底下,都會進行分類,並放到extensionClasses中。

當LoadingStrategy迴圈玩之後,最後將extensionClasses放入cachedClasses中,此時就完成了對於指定目錄下實現類的載入和分類。

至此,實現類的載入和分類就完成了。

5)實現類物件構造

看實現類物件構造過程之前,先看獲取,因為獲取不到才構造,也就是java中spi沒有的功能,按需載入。

獲取實現類物件的方法是getExtension方法,傳入的name引數就是短名稱,也就是spi檔案的鍵,wrap是是否包裝的意思,true的意思就是對你獲取的目標物件進行包裝(具體什麼是包裝,如何包裝後面會講),wrap預設是true

接下來我們就著重分析getExtension方法

圖片

前面兩個if我說一下,

第一個if比較簡單,就是簡單的引數校驗,name引數不能為空

第二個if判斷name是不是字串true,是的話就呼叫getDefaultExtension,getDefaultExtension這個方法通過名稱也能看出來就是獲取介面預設的實現,什麼是預設實現?預設的實現就是@SPI註解中的名稱對應的實現類。

前面兩個if之後就是真正獲取實現了。在獲取之前,先根據你是否包裝構建快取的鍵值,如果沒有包裝,就會在短名稱後加上 _origin  ,這主要是為了區分包不包裝,然後進入getOrCreateHolder方法

圖片

裡面其實就是通過快取名稱從cachedInstances獲取一個Holder,獲取不到就new一個Holder然後放到cachedInstances中,然後返回。Holder其實本身並沒有什麼意義,可以理解為一個空殼,裡面放的才是真正最終返回的物件。

第一次,不用說Holder肯定沒有,那麼這個Holder肯定是剛new出來的。

跳出getOrCreateHolder方法,繼續往下看。

圖片

從Holder中獲取實現類,此時肯定是null,接下來就是synchronized,然後又是非空判斷。這裡其實是典型的單例模式中的雙重檢查機制,保證併發安全。其實從這裡可以看出Holder的作用。這裡是為了減少鎖衝突的,因為一個實現類物件對應一個Holder物件,這樣不同的實現類在建立的時候,由於Holder的不同,synchronized就不是同一個鎖物件,這就起到了併發時候減少鎖衝突的作用,從這可以看出dubbo設計的時候的細節是很到位的。

 

第一次都是null,接下來進入createExtension方法,構建物件的過程

圖片

先從實現類的快取中獲取到短名稱對應的實現類,上面提到,實現類載入之後會放到內部的一個快取中。

 

這個if條件判斷一般肯定是false的,但是有些情況,就比如第一次構建物件丟擲異常,此時第二次來構建這個物件,那麼不用說肯定也會有問題,dubbo為了快速知道哪些實現類物件構造的時候會出異常,就在第一次構建物件拋異常的時候快取了實現類的短名稱到unacceptableExceptions中,當第二次來構建的時候,能夠快速知道,丟擲異常,減少資源的浪費。


接下來就會從extensionInstances中獲取例項,這個例項是沒有包裝的例項,也就是說如果你獲取的不帶包裝的例項,就是這個例項。我們看看這個例項是怎麼構建出來的,這裡我根據構建的不同階段進行劃分為以下幾個步驟。

第一步:例項化物件

通過例項化策略InstantiationStrategy進行例項化,預設是通過無參構造器構造的。圖片

第二步 :初始化前ExtensionPostProcessor 回撥

呼叫 ExtensionPostProcessor的postProcessBeforeInitialization方法,ExtensionPostProcessor跟spring中的BeanPostProcessor有點像,就是對目標物件進行擴充套件的作用。

圖片

第三步 :依賴注入

接下來呼叫injectExtension方法,這個方法就是依賴注入的實現方法。

依賴注入:說白了就是dubbo會自動呼叫需要依賴注入的方法,傳入相應的引數

哪些方法是需要依賴注入的方法?

dubbo約定 方法名以set開頭,方法的引數只有一個,方法上沒有加@DisableInject註解 ,方法是public的,符合這類的方法就是需要依賴注入的方法,dubbo在構建物件的時候會自動呼叫這些方法,傳入相應的引數。

 

接下來進入原始碼

圖片

 可以看出,先通過反射獲取到所有的方法,然後遍歷每個方法,進入兩個if判斷,這個判斷就是判斷是不是需要依賴注入的方法,也就是上面說的條件就在這個體現。

 

假設是需要依賴注入的方法,接下來看看如何獲得需要被注入的物件,也就是方法的引數。 

圖片

首先獲取需要set的物件的class型別,就是方法的引數型別

然後通過getSetterProperty方法獲取屬性名,可以理解為bean的名稱,

getSetterProperty就是方法去掉set然後第一個字母小寫之後就是屬性的名稱,舉個例子方法叫setUser,那麼屬性名就叫user,如果叫setUserName,屬性名就叫userName,就這麼簡單。 

最後就是根據屬性名和引數型別通過 ExtensionInjector 獲取需要被注入的物件。

 

ExtensionInjector 介面講解

ExtensionInjector就是注入器,通過這個可以獲取到被依賴注入的物件,這是個介面,有很多實現,這裡是 AdaptiveExtensionInjector 實現類,也是通過spi機制獲取的,ExtensionLoader構造的時候獲取的。

下面列舉了ExtensionInjector有的實現:

AdaptiveExtensionInjector:自適應的,本身沒有實際的意義,就是遍歷所有其它的ExtensionInjector實現來獲取,一旦有一個獲取到,就不會再呼叫下一個ExtensionInjector來獲取的

圖片

SpiExtensionInjector:顧名思義,就是通過spi機制來獲取,獲取的是自適應的實現

SpringExtensionInjector:這個是通過spring容器獲取實現,所以你通過dubbo的spi機制可以注入spring的bean

ScopeBeanExtensionInjector:通過dubbo內部的元件BeanFactory來獲取的,BeanFactory是dubbo內部用來在一定範圍的bean的容器,主要是為了物件的重複利用來的。

 

假設這裡獲取到了物件,那麼接下來就是通過反射呼叫set方法,進行依賴注入,然後依賴注入就完成了。

第四步:ExtensionAccessorAware介面回撥

如果你的介面實現了ExtensionAccessorAware介面,那麼會注入給你的bean一個 ExtensionDirector ,ExtensionDirector 可以想象成是ExtensionLoader工廠,可以獲取每個介面的ExtensionLoader。

第五步: 初始化後ExtensionPostProcessor回撥

 

圖片

呼叫ExtensionPostProcessor的postProcessAfterInitialization方法對目標物件進行擴充套件的作用。

第六步:自動包裝

到這一步實現類本身的物件就算構造好了,接下來就是進行自動包裝,如果wrap是true的話。 

自動包裝:可以說是靜態代理模式,就是對你的目標物件進行代理,怎麼代理,就是通過包裝類,什麼是包裝類,上文有說過,一個一個構造,慢慢構成一個呼叫鏈條,最終才會呼叫到真正的實現類

我們看看原始碼的實現

圖片

@Wrapper註解是個匹配的作用,就是根據需要屬性從包裝類中選擇一批可以用來包裝的類。

 

構造其實很簡單,就是當前instance當做包裝類的構造引數通過反射構造,然後進行依賴注入,然後將構造出來的物件複製給instance,instance再進行回撥之後再賦值給instance,這樣往往復復就形成了一個鏈條。這裡我畫個圖,讓大家看看最後構造出來的物件是什麼樣。

圖片

 構造後的物件其實就是這樣,你最終使用的物件其實是包裝物件,如果你獲取物件的時候傳的wrap引數是true的話,當前預設情況下是true。最後呼叫的話就會先呼叫最外層的包裝的方法(包裝物件2),然後呼叫(包裝物件1)一直呼叫,最後會呼叫到真正的目標物件的方法。

 

為什麼需要包裝?

很多人可能不清楚,為什麼需要包裝,其實很好理解,就是起到動態增強目標物件的作用。可以理解為spring中的aop,但是dubbo因為不像spring那樣有完整的ioc和aop的實現,dubbo就通過這種包裝的方式來實現動態增強目標物件功能的作用。 

第七步:Lifecycle介面回撥

接下來會呼叫initExtension方法,這個方法的作用就是判斷你的實現類有沒有實現Lifecycle介面,如果有的話會呼叫initialize()方法的實現 

至此,一個可用的實現類物件就算完完全全構建完成了,你拿到的物件就是這個物件,然後就會返回這個物件,存到Holder物件中。

圖片 

最後來張圖總結一下實現類構造的過程。

 

圖片

這裡我在簡單說明一下,

1)包裝不是必須的,得看你要獲取的物件是什麼,如果不要包裝,就會回撥原始物件的Lifecycle介面,不過dubbo內部的框架基本上獲取的都是帶包裝的物件,而非原始的物件;

2)包裝時暴露出去的是包裝類的物件,在呼叫的時候,最先呼叫的也是包裝類的物件,然後一層一層的呼叫,最終呼叫到實現類物件。

6)自適應機制 

自適應:自適應擴充套件類的含義是說,基於引數,在執行時動態選擇到具體的目標類,然後執行。在 Dubbo 中,很多擴充都是通過 SPI 機制進行載入的,比如 Protocol、Cluster、LoadBalance 等。有時,有些擴充並不想在框架啟動階段被載入,而是希望在擴充方法被呼叫時,根據執行時引數進行載入。這聽起來有些矛盾。擴充未被載入,那麼擴充方法就無法被呼叫(靜態方法除外)。擴充方法未被呼叫,擴充就無法被載入。對於這個矛盾的問題,Dubbo 通過自適應擴充機制很好的解決了。自適應擴充機制的實現邏輯比較複雜,首先 Dubbo 會為擴充介面生成具有代理功能的程式碼。然後通過 javassist 或 jdk 編譯這段程式碼,得到 Class 類。最後再通過反射建立代理類,整個過程比較複雜。

自適應物件獲取的方法,就是getAdaptiveExtension方法。構建自適應物件的方法就是createAdaptiveExtension方法的實現。

圖片

原始碼很簡單,就是得到自適應的實現類,然後就是普通反射構造,然後經過初始化前,依賴注入,初始化之後,Lifecycle介面回撥操作,構造出物件。

自適應的類有兩種來源,一種是自己在實現類上加@Adaptive註解,指定自適應實現類,上面提到的AdaptiveExtensionInjector就是指定的自適應實現類,類上加了@Adaptive註解,如果不指定,dubbo框架會按照一定的規則來動態生成一個自適應的類,構造過程在createAdaptiveExtensionClass方法實現,最終會呼叫AdaptiveClassCodeGenerator生成程式碼

圖片

 

7)自動啟用 

所謂的自動啟用,就是根據你的入參,動態的選擇一批實現類返回給你。至於怎麼找到,就是通過註解@Activate來實現的。dubbo內部自動啟用的主要用在Filter中,Filter是個介面,有很多實現。不論是在provider端還是consumer端,在呼叫之前,都會經過一個由Filter實現構成的鏈,這條鏈的不同實現就是根據入參的不同來區分是每個Filter的實現屬於provider的還是consumer端的。

 

@Activate它有三個重要屬性,group 表示修飾在哪個端,是 provider 還是 consumer,value 表示在 URL引數中出現才會被啟用,order 表示實現類的順序。

圖片

 

總結

本文先介紹了什麼是spi機制,然後分析了java的spi機制和spring的spi機制,最後我們進入本文的主題,dubbo的spi機制,我們從原始碼的角度剖析了dubbo spi機制的功能,包擴了在構建物件時的實現類的載入、ioc和自動包裝的機制、自適應物件機制、自動啟用機制。整體而言,dubbo的spi機制不是很難,所以大家看了兩篇文章之後如果自己再過一遍原始碼的話那麼收穫會更大。dubbo的spi機制其實非常重要,如果不理解dubbo的spi機制的特性的話,在閱讀dubbo原始碼的時候,很難讀懂,因為你可能都不知道,你拿到的物件到底是什麼樣的,這樣就很難理解一些功能的實現。

 

往期熱門文章推薦

掃碼或者搜尋關注公眾號 三友的java日記 ,及時乾貨不錯過,公眾號致力於通過畫圖加上通俗易懂的語言講解技術,讓技術更加容易學習。 

相關文章