天天看點

聊聊Dubbo - Dubbo可擴充機制實戰

1. Dubbo的擴充機制

在Dubbo的官網上,Dubbo描述自己是一個高性能的RPC架構。今天我想聊聊Dubbo的另一個很棒的特性, 就是它的可擴充性。 如同羅馬不是一天建成的,任何系統都一定是從小系統不斷發展成為大系統的,想要從一開始就把系統設計的足夠完善是不可能的,相反的,我們應該關注當下的需求,然後再不斷地對系統進行疊代。在代碼層面,要求我們适當的對關注點進行抽象和隔離,在軟體不斷添加功能和特性時,依然能保持良好的結構和可維護性,同時允許第三方開發者對其功能進行擴充。在某些時候,軟體設計者對擴充性的追求甚至超過了性能。

在談到軟體設計時,可擴充性一直被談起,那到底什麼才是可擴充性,什麼樣的架構才算有良好的可擴充性呢?它必須要做到以下兩點:

  1. 作為架構的維護者,在添加一個新功能時,隻需要添加一些新代碼,而不用大量的修改現有的代碼,即符合開閉原則。
  2. 作為架構的使用者,在添加一個新功能時,不需要去修改架構的源碼,在自己的工程中添加代碼即可。

    Dubbo很好的做到了上面兩點。這要得益于Dubbo的微核心+插件的機制。接下來的章節中我們會慢慢揭開Dubbo擴充機制的神秘面紗。

2. 可擴充的幾種解決方案

通常可擴充的實作有下面幾種:

  • Factory模式
  • IoC容器
  • OSGI容器

    Dubbo作為一個架構,不希望強依賴其他的IoC容器,比如Spring,Guice。OSGI也是一個很重的實作,不适合Dubbo。最終Dubbo的實作參考了Java原生的SPI機制,但對其進行了一些擴充,以滿足Dubbo的需求。

3. Java SPI機制

既然Dubbo的擴充機制是基于Java原生的SPI機制,那麼我們就先來了解下Java SPI吧。了解了Java的SPI,也就是對Dubbo的擴充機制有一個基本的了解。如果對Java SPI比較了解的同學,可以跳過。

Java SPI(Service Provider Interface)是JDK内置的一種動态加載擴充點的實作。在ClassPath的META-INF/services目錄下放置一個與接口同名的文本檔案,檔案的内容為接口的實作類,多個實作類用換行符分隔。JDK中使用java.util.ServiceLoader來加載具體的實作。 讓我們通過一個簡單的例子,來看看Java SPI是如何工作的。

  1. 定義一個接口IRepository用于實作資料儲存
  2. interface IRepository { void save(String data); }
  3. 提供IRepository的實作 IRepository有兩個實作。MysqlRepository和MongoRepository。
  4. class MysqlRepository implements IRepository { public void save(String data) { System.out.println("Save " + data + " to Mysql"); } }

public class MongoRepository implements IRepository { public void save(String data) { System.out.println("Save " + data + " to Mongo"); } }

  1. 添加配置檔案 在META-INF/services目錄添加一個檔案,檔案名和接口全名稱相同,是以檔案是META-INF/services/com.demo.IRepository。檔案内容為:

    com.demo.MongoRepository com.demo.MysqlRepository

  2. 通過ServiceLoader加載IRepository實作

    ServiceLoader serviceLoader = ServiceLoader.load(IRepository.class); Iterator it = serviceLoader.iterator(); while (it != null && it.hasNext()){ IRepository demoService = it.next(); System.out.println("class:" + demoService.getClass().getName()); demoService.save("tom"); }

在上面的例子中,我們定義了一個擴充點和它的兩個實作。在ClassPath中添加了擴充的配置檔案,最後使用ServiceLoader來加載所有的擴充點。

4. Dubbo的SPI機制

Java SPI的使用很簡單。也做到了基本的加載擴充點的功能。但Java SPI有以下的不足:

  • 需要周遊所有的實作,并執行個體化,然後我們在循環中才能找到我們需要的實作。
  • 配置檔案中隻是簡單的列出了所有的擴充實作,而沒有給他們命名。導緻在程式中很難去準确的引用它們。
  • 擴充如果依賴其他的擴充,做不到自動注入和裝配
  • 不提供類似于Spring的AOP功能
  • 擴充很難和其他的架構內建,比如擴充裡面依賴了一個Spring bean,原生的Java SPI不支援

    是以Java SPI應付一些簡單的場景是可以的,但對于Dubbo,它的功能還是比較弱的。Dubbo對原生SPI機制進行了一些擴充。接下來,我們就更深入地了解下Dubbo的SPI機制。

5. Dubbo擴充點機制基本概念

在深入學習Dubbo的擴充機制之前,我們先明确Dubbo SPI中的一些基本概念。在接下來的内容中,我們會多次用到這些術語。

  1. 擴充點(Extension Point)

    是一個Java的接口。

  2. 擴充(Extension)

    擴充點的實作類。

  3. 擴充執行個體(Extension Instance)

    擴充點實作類的執行個體。

  4. 擴充自适應執行個體(Extension Adaptive Instance)

    第一次接觸這個概念時,可能不太好了解(我第一次也是這樣的...)。如果稱它為擴充代理類,可能更好了解些。擴充的自适應執行個體其實就是一個Extension的代理,它實作了擴充點接口。在調用擴充點的接口方法時,會根據實際的參數來決定要使用哪個擴充。比如一個IRepository的擴充點,有一個save方法。有兩個實作MysqlRepository和MongoRepository。IRepository的自适應執行個體在調用接口方法的時候,會根據save方法中的參數,來決定要調用哪個IRepository的實作。如果方法參數中有repository=mysql,那麼就調用MysqlRepository的save方法。如果repository=mongo,就調用MongoRepository的save方法。和面向對象的延遲綁定很類似。為什麼Dubbo會引入擴充自适應執行個體的概念呢?

    • Dubbo中的配置有兩種,一種是固定的系統級别的配置,在Dubbo啟動之後就不會再改了。還有一種是運作時的配置,可能對于每一次的RPC,這些配置都不同。比如在xml檔案中配置了逾時時間是10秒鐘,這個配置在Dubbo啟動之後,就不會改變了。但針對某一次的RPC調用,可以設定它的逾時時間是30秒鐘,以覆寫系統級别的配置。對于Dubbo而言,每一次的RPC調用的參數都是未知的。隻有在運作時,根據這些參數才能做出正确的決定。
    • 很多時候,我們的類都是一個單例的,比如Spring的bean,在Spring bean都執行個體化時,如果它依賴某個擴充點,但是在bean執行個體化時,是不知道究竟該使用哪個具體的擴充實作的。這時候就需要一個代理模式了,它實作了擴充點接口,方法内部可以根據運作時參數,動态的選擇合适的擴充實作。而這個代理就是自适應執行個體。 自适應擴充執行個體在Dubbo中的使用非常廣泛,Dubbo中,每一個擴充都會有一個自适應類,如果我們沒有提供,Dubbo會使用位元組碼工具為我們自動生成一個。是以我們基本感覺不到自适應類的存在。後面會有例子說明自适應類是怎麼工作的。
  5. @SPI

    @SPI注解作用于擴充點的接口上,表明該接口是一個擴充點。可以被Dubbo的ExtentionLoader加載。如果沒有此ExtensionLoader調用會異常。

  6. @Adaptive

    @Adaptive注解用在擴充接口的方法上。表示該方法是一個自适應方法。Dubbo在為擴充點生成自适應執行個體時,如果方法有@Adaptive注解,會為該方法生成對應的代碼。方法内部會根據方法的參數,來決定使用哪個擴充。

  7. ExtentionLoader

    類似于Java SPI的ServiceLoader,負責擴充的加載和生命周期維護。

  8. 擴充别名

    和Java SPI不同,Dubbo中的擴充都有一個别名,用于在應用中引用它們。比如

random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance

其中的random,roundrobin就是對應擴充的别名。這樣我們在配置檔案中使用random或roundrobin就可以了。

  1. 一些路徑

    和Java SPI從/META-INF/services目錄加載擴充配置類似,Dubbo也會從以下路徑去加載擴充配置檔案:

  • META-INF/dubbo/internal
  • META-INF/dubbo
  • META-INF/services

6. Dubbo的LoadBalance擴充點解讀

在了解了Dubbo的一些基本概念後,讓我們一起來看一個Dubbo中實際的擴充點,對這些概念有一個更直覺的認識。

我們選擇的是Dubbo中的LoadBalance擴充點。Dubbo中的一個服務,通常有多個Provider,consumer調用服務時,需要在多個Provider中選擇一個。這就是一個LoadBalance。我們一起來看看在Dubbo中,LoadBalance是如何成為一個擴充點的。

  1. LoadBalance接口

    @SPI(RandomLoadBalance.NAME) public interface LoadBalance { @Adaptive("loadbalance") Invoker select(List> invokers, URL url, Invocation invocation) throws RpcException; }

LoadBalance接口隻有一個select方法。select方法從多個invoker中選擇其中一個。上面代碼中和Dubbo SPI相關的元素有:

  • @SPI(RandomLoadBalance.NAME) @SPI作用于LoadBalance接口,表示接口LoadBalance是一個擴充點。如果沒有@SPI注解,試圖去加載擴充時,會抛出異常。@SPI注解有一個參數,該參數表示該擴充點的預設實作的别名。如果沒有顯示的指定擴充,就使用預設實作。RandomLoadBalance.NAME是一個常量,值是"random",是一個随機負載均衡的實作。 random的定義在配置檔案META-INF/dubbo/internal/com.alibaba.dubbo.rpc.cluster.LoadBalance中:

    random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance leastactive=com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance consistenthash=com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance

可以看到檔案中定義了4個LoadBalance的擴充實作。由于負載均衡的實作不是本次的内容,這裡就不過多說明。隻用知道Dubbo提供了4種負載均衡的實作,我們可以通過xml檔案,properties檔案,JVM參數顯式的指定一個實作。如果沒有,預設使用随機。

聊聊Dubbo - Dubbo可擴充機制實戰
  • @Adaptive("loadbalance") @Adaptive注解修飾select方法,表明方法select方法是一個可自适應的方法。Dubbo會自動生成該方法對應的代碼。當調用select方法時,會根據具體的方法參數來決定調用哪個擴充實作的select方法。@Adaptive注解的參數loadbalance表示方法參數中的loadbalance的值作為實際要調用的擴充執行個體。 但奇怪的是,我們發現select的方法中并沒有loadbalance參數,那怎麼擷取loadbalance的值呢?select方法中還有一個URL類型的參數,Dubbo就是從URL中擷取loadbalance的值的。這裡涉及到Dubbo的URL總線模式,簡單說,URL中包含了RPC調用中的所有參數。URL類中有一個Map parameters字段,parameters中就包含了loadbalance。
  1. 擷取LoadBalance擴充

    Dubbo中擷取LoadBalance的代碼如下:

LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName);

使用ExtensionLoader.getExtensionLoader(LoadBalance.class)方法擷取一個ExtensionLoader的執行個體,然後調用getExtension,傳入一個擴充的别名來擷取對應的擴充執行個體。

7. 自定義一個LoadBalance擴充

本節中,我們通過一個簡單的例子,來自己實作一個LoadBalance,并把它內建到Dubbo中。我會列出一些關鍵的步驟和代碼,也可以從這個位址(

https://github.com/vangoleo/dubbo-spi-demo)

下載下傳完整的demo。

  1. 實作LoadBalance接口

    首先,編寫一個自己實作的LoadBalance,因為是為了示範Dubbo的擴充機制,而不是LoadBalance的實作,是以這裡LoadBalance的實作非常簡單,選擇第一個invoker,并在控制台輸出一條日志。

package com.dubbo.spi.demo.consumer; public class DemoLoadBalance implements LoadBalance { @Override public Invoker select(List> invokers, URL url, Invocation invocation) throws RpcException { System.out.println("DemoLoadBalance: Select the first invoker..."); return invokers.get(0); } }

  1. 添加擴充配置檔案

    添加檔案:META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance。檔案内容如下:

demo=com.dubbo.spi.demo.consumer.DemoLoadBalance

  1. 配置使用自定義LoadBalance

    通過上面的兩步,已經添加了一個名字為demo的LoadBalance實作,并在配置檔案中進行了相應的配置。接下來,需要顯式的告訴Dubbo使用demo的負載均衡實作。如果是通過spring的方式使用Dubbo,可以在xml檔案中進行設定。

在consumer端的dubbo:reference中配置

  1. 啟動Dubbo

    啟動Dubbo,調用一次IHelloService,可以看到控制台會輸出一條DemoLoadBalance: Select the first invoker...日志。說明Dubbo的确是使用了我們自定義的LoadBalance。

總結

到此,我們從Java SPI開始,了解了Dubbo SPI 的基本概念,并結合了Dubbo中的LoadBalance加深了了解。最後,我們還實踐了一下,建立了一個自定義LoadBalance,并內建到Dubbo中。相信通過這裡理論和實踐的結合,大家對Dubbo的可擴充有更深入的了解。

總結一下,Dubbo SPI有以下的特點:

• 對Dubbo進行擴充,不需要改動Dubbo的源碼

• 自定義的Dubbo的擴充點實作,是一個普通的Java類,Dubbo沒有引入任何Dubbo特有的元素,對代碼侵入性幾乎為零。

• 将擴充注冊到Dubbo中,隻需要在ClassPath中添加配置檔案。使用簡單。而且不會對現有代碼造成影響。符合開閉原則。

• Dubbo的擴充機制支援IoC,AoP等進階功能

• Dubbo的擴充機制能很好的支援第三方IoC容器,預設支援Spring Bean,可自己擴充來支援其他容器,比如Google的Guice。

• 切換擴充點的實作,隻需要在配置檔案中修改具體的實作,不需要改代碼。使用友善。

下一篇,我們将會一起深入Dubbo的源碼,更深入的了解Dubbo的可擴充機制。

第四屆阿裡中間件性能挑戰賽火熱進行中,挑戰Dubbo Agent,赢取50萬現金大獎,參賽請戳【 大賽直通車