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

weixin_34127717發表於2018-06-04

摘要: 在Dubbo的官網上,Dubbo描述自己是一個高效能的RPC框架。今天我想聊聊Dubbo的另一個很棒的特性, 就是它的可擴充套件性。

  1. Dubbo的擴充套件機制

在Dubbo的官網上,Dubbo描述自己是一個高效能的RPC框架。今天我想聊聊Dubbo的另一個很棒的特性, 就是它的可擴充套件性。 如同羅馬不是一天建成的,任何系統都一定是從小系統不斷髮展成為大系統的,想要從一開始就把系統設計的足夠完善是不可能的,相反的,我們應該關注當下的需求,然後再不斷地對系統進行迭代。在程式碼層面,要求我們適當的對關注點進行抽象和隔離,在軟體不斷新增功能和特性時,依然能保持良好的結構和可維護性,同時允許第三方開發者對其功能進行擴充套件。在某些時候,軟體設計者對擴充套件性的追求甚至超過了效能。
在談到軟體設計時,可擴充套件性一直被談起,那到底什麼才是可擴充套件性,什麼樣的框架才算有良好的可擴充套件性呢?它必須要做到以下兩點:

作為框架的維護者,在新增一個新功能時,只需要新增一些新程式碼,而不用大量的修改現有的程式碼,即符合開閉原則。
作為框架的使用者,在新增一個新功能時,不需要去修改框架的原始碼,在自己的工程中新增程式碼即可。
Dubbo很好的做到了上面兩點。這要得益於Dubbo的微核心+外掛的機制。接下來的章節中我們會慢慢揭開Dubbo擴充套件機制的神祕面紗。

  1. 可擴充套件的幾種解決方案

通常可擴充套件的實現有下面幾種:

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

  1. 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是如何工作的。

定義一個介面IRepository用於實現資料儲存
interface IRepository { void save(String data); }
提供IRepository的實現 IRepository有兩個實現。MysqlRepository和MongoRepository。
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"); } }

新增配置檔案 在META-INF/services目錄新增一個檔案,檔名和介面全名稱相同,所以檔案是META-INF/services/com.demo.IRepository。檔案內容為:
com.demo.MongoRepository com.demo.MysqlRepository
通過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來載入所有的擴充套件點。

  1. Dubbo的SPI機制

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

需要遍歷所有的實現,並例項化,然後我們在迴圈中才能找到我們需要的實現。
配置檔案中只是簡單的列出了所有的擴充套件實現,而沒有給他們命名。導致在程式中很難去準確的引用它們。
擴充套件如果依賴其他的擴充套件,做不到自動注入和裝配
不提供類似於Spring的AOP功能
擴充套件很難和其他的框架整合,比如擴充套件裡面依賴了一個Spring bean,原生的Java SPI不支援
所以Java SPI應付一些簡單的場景是可以的,但對於Dubbo,它的功能還是比較弱的。Dubbo對原生SPI機制進行了一些擴充套件。接下來,我們就更深入地瞭解下Dubbo的SPI機制。

  1. Dubbo擴充套件點機制基本概念

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

擴充套件點(Extension Point)
是一個Java的介面。
擴充套件(Extension)
擴充套件點的實現類。
擴充套件例項(Extension Instance)
擴充套件點實現類的例項。
擴充套件自適應例項(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會使用位元組碼工具為我們自動生成一個。所以我們基本感覺不到自適應類的存在。後面會有例子說明自適應類是怎麼工作的。
@SPI
@SPI註解作用於擴充套件點的介面上,表明該介面是一個擴充套件點。可以被Dubbo的ExtentionLoader載入。如果沒有此ExtensionLoader呼叫會異常。
@Adaptive
@Adaptive註解用在擴充套件介面的方法上。表示該方法是一個自適應方法。Dubbo在為擴充套件點生成自適應例項時,如果方法有@Adaptive註解,會為該方法生成對應的程式碼。方法內部會根據方法的引數,來決定使用哪個擴充套件。
ExtentionLoader
類似於Java SPI的ServiceLoader,負責擴充套件的載入和生命週期維護。
擴充套件別名
和Java SPI不同,Dubbo中的擴充套件都有一個別名,用於在應用中引用它們。比如
random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
其中的random,roundrobin就是對應擴充套件的別名。這樣我們在配置檔案中使用random或roundrobin就可以了。

一些路徑
和Java SPI從/META-INF/services目錄載入擴充套件配置類似,Dubbo也會從以下路徑去載入擴充套件配置檔案:
META-INF/dubbo/internal
META-INF/dubbo
META-INF/services

  1. Dubbo的LoadBalance擴充套件點解讀

在瞭解了Dubbo的一些基本概念後,讓我們一起來看一個Dubbo中實際的擴充套件點,對這些概念有一個更直觀的認識。
我們選擇的是Dubbo中的LoadBalance擴充套件點。Dubbo中的一個服務,通常有多個Provider,consumer呼叫服務時,需要在多個Provider中選擇一個。這就是一個LoadBalance。我們一起來看看在Dubbo中,LoadBalance是如何成為一個擴充套件點的。

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引數顯式的指定一個實現。如果沒有,預設使用隨機。
圖片描述

@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。
獲取LoadBalance擴充套件
Dubbo中獲取LoadBalance的程式碼如下:
LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName);
使用ExtensionLoader.getExtensionLoader(LoadBalance.class)方法獲取一個ExtensionLoader的例項,然後呼叫getExtension,傳入一個擴充套件的別名來獲取對應的擴充套件例項。

  1. 自定義一個LoadBalance擴充套件

本節中,我們通過一個簡單的例子,來自己實現一個LoadBalance,並把它整合到Dubbo中。我會列出一些關鍵的步驟和程式碼,也可以從這個地址(https://github.com/vangoleo/d...

實現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); } }

新增擴充套件配置檔案
新增檔案:META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance。檔案內容如下:
demo=com.dubbo.spi.demo.consumer.DemoLoadBalance

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

在consumer端的dubbo:reference中配置

啟動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。
• 切換擴充套件點的實現,只需要在配置檔案中修改具體的實現,不需要改程式碼。使用方便。

原文連結

相關文章