天天看點

10年開發大牛帶你深度解析微服務高并發:SPI在Sentinel中的應用

作者:程式員進階碼農II

SPI在Sentinel中的應用

SPI機制在阿裡巴巴集團開源的項目中被廣泛使用,如Dubbo、RocketMQ與Sentinel都使用了SPI機制。除Dubbo外,RocketMQ與Sentinel使用的都是Java提供的SPI機制。

Dubbo使用的是自實作的一套SPI——Dubbo SPI,與Java SPI的配置方式不同,DubboSPI使用Key-Value方式配置,目的是實作自适應擴充機制。

Java SPI實作原理與适用場景

SPI(Service Provider Interface,服務提供者接口)是一種服務發現機制,是Java的一個内置标準,可以保障不同的開發者實作某個特定的服務。

SPI的本質是将接口實作類的全限定名配置在檔案中,由服務加載器讀取配置檔案、加載實作類并建立執行個體。使用SPI機制能夠實作運作時從配置檔案中讀取接口的實作類并建立執行個體。

我們以實作動态切換登入方式為例講解如何使用Java SPI,雖然這不是一個很好的使用案例,但是通過此案例能更直覺地認識Java SPI。

1. 定義登入接口

定義登入接口LoginService,該接口提供了login方法,login方法可以接收使用者名和密碼并傳回登入結果。接口定義的代碼如下。

10年開發大牛帶你深度解析微服務高并發:SPI在Sentinel中的應用

2. 編寫接口實作類

如果想使用Shiro架構實作使用者鑒權,那麼需要提供一個LoginService的實作類ShiroLoginService。使用ShiroLoginService類的代碼如下。

10年開發大牛帶你深度解析微服務高并發:SPI在Sentinel中的應用

如果想直接使用Spring MVC的攔截器實作使用者鑒權,那麼需要提供一個LoginService的實作類SpringLoginService。使用SpringLoginService類的代碼如下。

10年開發大牛帶你深度解析微服務高并發:SPI在Sentinel中的應用

3. 通過配置使用SpringLoginService類或ShiroLoginService類

當我們想通過修改配置檔案的方式而不是修改代碼的方式實作權限驗證架構的切換時,就可以使用Java SPI,具體做法是運作時從配置檔案中讀取LoginService的實作類,然後加載并使用配置的實作類。

首先,在項目的resources目錄下建立META-INF目錄,并在META-INF目錄下建立services目錄;然後,在services目錄下建立名稱為LoginService的配置檔案(LoginService是接口的全類名);最後,在配置檔案中寫入使用的LoginService接口實作類的全類名。

提示:隻要在META-INF/services目錄下,且檔案名是接口的全類名,在編寫配置檔案内容時,IDEA就會自動提示有哪些實作類。

在配置檔案中,填寫的内容為接口的實作類,多個實作類使用換行的方式分開。在此案例中,如果想使用ShiroLoginService類,則配置如下。

10年開發大牛帶你深度解析微服務高并發:SPI在Sentinel中的應用

4. 使用Java SPI加載LoginService

編寫main方法,測試使用ServiceLoader加載LoginService,代碼如下。

10年開發大牛帶你深度解析微服務高并發:SPI在Sentinel中的應用

ServiceLoader是Java提供的服務加載器,用于實作SPI機制。ServiceLoader提供load靜态方法,該方法接收一個接口并傳回一個ServiceLoader執行個體。通過周遊ServiceLoader執行個體的疊代器(Iterator),我們可以擷取接口對應的配置檔案中配置的所有實作類執行個體。

在調用ServiceLoader#load方法後,此時配置檔案中注冊的實作類還沒有被加載到JVM中,隻有通過疊代器周遊擷取時,才會加載實作類及執行個體化實作類,并且周遊的順序就是配置檔案中注冊實作類的順序。

在本例中,我們通過forEach文法周遊ServiceLoader時,使用了break語句,因為在登入場景下,不可能同時使用兩種LoginService,是以也不應該在SPI配置檔案中配置多個LoginService的實作類。

➢ ServiceLoader實作原理

在調用ServiceLoader#load方法時,ServiceLoader根據參數傳入的接口擷取接口的全類名,将字首/META-INF/services與接口的全類名拼接定位到配置檔案,然後讀取配置檔案中的字元串并解析字元串,将解析出來的實作類全類名添加到一個數組中,并傳回一個ServiceLoader執行個體。

ServiceLoader實作了Iterable接口,是以可以使用forEach文法周遊。ServiceLoader使用lazy方式(“懶加載”或“延遲加載”)實作疊代器,隻有被疊代器的next方法周遊到的類才會被加載和執行個體化。如果隻想使用接口配置檔案中注冊的第一個實作類,那麼在使用疊代器周遊時,可以使用break語句跳出循環。

在使用疊代器周遊時,ServiceLoader通過調用Class#forName方法加載類并且通過反射建立執行個體。如果不指定加載實作類使用的類加載器,ServiceLoader就會使用目前線程的上下文類加載器加載。

➢ SPI機制的适用場景

适合使用政策模式、責任鍊模式的場景都可以使用SPI機制。

例如,将SPI機制用在繪制形狀的場景:定義一個形狀接口,實作矩形、三角形等的繪制,如果想要添加圓形,隻需要在形狀接口的配置檔案中注冊圓形即可支援繪制圓形,完全不用修改任何代碼。

Java SPI在Sentinel中的應用

Sentinel使用Java SPI為我們提供了插件注冊的功能,類似于Spring Boot提供的自動配置類注冊功能。

我們可以直接替換Sentinel提供的預設SlotChainBuilder,使用自定義的SlotChainBuilder為資源構造自己的ProcessorSlotChain,以實作修改ProcessorSlot排列順序、增加或移除ProcessorSlot的功能。

提示:在Sentinel 1.7.2版本中,Sentinel支援使用SPI注冊ProcessorSlot,并且支援排序。

在sentinel-core子產品的resources資源目錄下,有一個META-INF/services目錄,該目錄下有兩個以接口全類名命名的檔案。其中,com.alibaba.csp.sentinel.slotchain.SlotChainBuilder檔案用于配置SlotChainBuilder接口的實作類,而com.alibaba.csp.sentinel.init.InitFunc檔案用于配置InitFunc接口的實作類,并且這兩個配置檔案中都配置了接口的預設實作類,如果不添加新的配置,Sentinel将使用預設配置的接口實作類。

• com.alibaba.csp.sentinel.slotchain.SlotChainBuilder檔案的預設配置如下。

10年開發大牛帶你深度解析微服務高并發:SPI在Sentinel中的應用

• com.alibaba.csp.sentinel.init.InitFunc檔案的預設配置如下。

10年開發大牛帶你深度解析微服務高并發:SPI在Sentinel中的應用

ServiceLoader可加載接口配置檔案中配置的所有實作類,并且使用反射建立對象,但是是否全部加載及執行個體化仍然由使用者自己決定。

sentinel-core子產品在使用Java SPI機制加載InitFunc與SlotChainBuilder時,會在實作上稍有不同。如果InitFunc接口的配置檔案注冊了多個實作類,那麼這些注冊的InitFunc實作類都會被Sentinel加載并執行個體化,且都會被使用。但是如果SlotChainBuilder接口的配置檔案注冊了多個實作類,那麼Sentinel隻會加載和使用第一個實作類。

Sentinel在加載SlotChainBuilder時,隻會擷取第一個非預設實作類的執行個體,如果接口配置檔案中隻有預設實作類而沒有注冊其他的實作類,那麼Sentinel會使用這個預設的SlotChainBuilder。實作源碼在SpiLoader的loadFirstInstanceOrDefault方法中,代碼如下。

10年開發大牛帶你深度解析微服務高并發:SPI在Sentinel中的應用

① 擷取接口的ServiceLoader執行個體。

② 周遊加載接口的實作類并擷取執行個體,若擷取的執行個體類型與指定的預設實作類不同,則使用該執行個體。

③ 使用預設實作類的執行個體,使用反射建立預設實作類的執行個體。

因為Sentinel允許存在多個初始化方法,是以Sentinel加載InitFunc與SlotChainBuilder的方式會有所不同。Sentinel使用ServiceLoader加載注冊的InitFunc實作類執行個體的代碼如下。

10年開發大牛帶你深度解析微服務高并發:SPI在Sentinel中的應用

① 周遊擷取注冊的所有接口實作類的執行個體。

② 實作排序及包裝執行個體。

③ 周遊調用每個InitFunc執行個體的初始化方法。

InitFunc可用于初始化配置限流、熔斷規則,但在Web項目中基本不會使用它,更多的是先通過監聽Spring容器重新整理完成事件,再初始化Sentinel配置規則。如果使用Sentinel提供的動态資料源還可以在監聽到動态配置改變事件時重新加載規則,則基本使用不到InitFunc。

雖然InitFunc接口與SlotChainBuilder接口的配置檔案在sentinel-core子產品下,但是并不需要修改Sentinel的源碼,也不需要修改sentinel-core子產品下的接口配置檔案,而隻需要在目前項目的resource/META-INF/services目錄下建立一個與接口名稱相同的配置檔案,并在配置檔案中添加接口的實作類即可。ServiceLoader會周遊項目依賴的每個jar包下的與接口名稱相同的配置檔案。

ServiceLoader會周遊項目依賴的每個jar包下的與接口名稱相同的配置檔案,同一個檔案中注冊的實作類是按注冊順序周遊的,但多個檔案的周遊順序則是不确定的,是以不要依賴注冊順序實作排序。

自定義組裝ProcessorSlotChain

Sentinel使用SlotChainBuilder将多個ProcessorSlot構造成一個ProcessorSlotChain,由ProcessorSlotChain按照ProcessorSlot的注冊順序排程這些ProcessorSlot。

Sentinel使用Java SPI加載SlotChainBuilder,支援使用者自定義SlotChainBuilder,相當于提供了插件的功能。

預設使用的SlotChainBuilder是DefaultSlotChainBuilder。DefaultSlotChainBuilder構造ProcessorSlotChain的源碼如下。

10年開發大牛帶你深度解析微服務高并發:SPI在Sentinel中的應用

•build方法:用來建立DefaultProcessorSlotChain執行個體,并注冊ProcessorSlot,如NodeSelectorSlot、ClusterBuilderSlot、LogSlot、StatisticSlot、AuthoritySlot、SystemSlot、FlowSlot、DegradeSlot。

DefaultSlotChainBuilder注冊的ProcessorSlot并非都是必需的,如果注冊的ProcessorSlot中有些用不到,那麼可以自己實作一個SlotChainBuilder,自己構造ProcessorSlotChain。

例如,可以将LogSlot、AuthoritySlot和SystemSlot去掉,實作這個操作隻需要兩步。

(1)編寫MySlotChainBuilder類,實作SlotChainBuilder接口,代碼如下。

10年開發大牛帶你深度解析微服務高并發:SPI在Sentinel中的應用

(2)在目前項目的resources/META-INF/services目錄下添加名稱為com.alibaba.csp.sentinel.

slotchain.SlotChainBuilder的接口配置檔案,并在配置檔案中注冊MySlotChainBuilder類,配置代碼如下。

10年開發大牛帶你深度解析微服務高并發:SPI在Sentinel中的應用

需要注意的是ProcessorSlot的注冊順序,NodeSelectorSlot需要作為ClusterBuilderSlot的前驅節點,ClusterBuilderSlot需要作為StatisticSlot的前驅節點,否則Sentinel運作會出現Bug。但可以将DegradeSlot放在FlowSlot的前面。

本文給大家講解的内容是深度解析微服務高并發了解整體工作流程:SPI在Sentinel中的應用

  1. 下篇文章給大家講解的内容是深度解析微服務高并發了解整體工作流程:責任鍊模式在Sentinel中的應用
  2. 感謝大家的支援!

繼續閱讀