摘要:在Dubbo的官網(wǎng)上驯击,Dubbo描述自己是一個(gè)高性能的RPC框架。今天我想聊聊Dubbo的另一個(gè)很棒的特性, 就是它的可擴(kuò)展性赠摇。
1. Dubbo的擴(kuò)展機(jī)制
在Dubbo的官網(wǎng)上芍瑞,Dubbo描述自己是一個(gè)高性能的RPC框架。今天我想聊聊Dubbo的另一個(gè)很棒的特性, 就是它的可擴(kuò)展性皮胡。 如同羅馬不是一天建成的痴颊,任何系統(tǒng)都一定是從小系統(tǒng)不斷發(fā)展成為大系統(tǒng)的,想要從一開(kāi)始就把系統(tǒng)設(shè)計(jì)的足夠完善是不可能的屡贺,相反的蠢棱,我們應(yīng)該關(guān)注當(dāng)下的需求锌杀,然后再不斷地對(duì)系統(tǒng)進(jìn)行迭代。在代碼層面泻仙,要求我們適當(dāng)?shù)膶?duì)關(guān)注點(diǎn)進(jìn)行抽象和隔離糕再,在軟件不斷添加功能和特性時(shí),依然能保持良好的結(jié)構(gòu)和可維護(hù)性玉转,同時(shí)允許第三方開(kāi)發(fā)者對(duì)其功能進(jìn)行擴(kuò)展突想。在某些時(shí)候,軟件設(shè)計(jì)者對(duì)擴(kuò)展性的追求甚至超過(guò)了性能究抓。
在談到軟件設(shè)計(jì)時(shí)猾担,可擴(kuò)展性一直被談起,那到底什么才是可擴(kuò)展性刺下,什么樣的框架才算有良好的可擴(kuò)展性呢绑嘹?它必須要做到以下兩點(diǎn):
作為框架的維護(hù)者,在添加一個(gè)新功能時(shí)怠李,只需要添加一些新代碼圾叼,而不用大量的修改現(xiàn)有的代碼,即符合開(kāi)閉原則捺癞。
作為框架的使用者夷蚊,在添加一個(gè)新功能時(shí),不需要去修改框架的源碼髓介,在自己的工程中添加代碼即可惕鼓。
Dubbo很好的做到了上面兩點(diǎn)。這要得益于Dubbo的微內(nèi)核+插件的機(jī)制唐础。接下來(lái)的章節(jié)中我們會(huì)慢慢揭開(kāi)Dubbo擴(kuò)展機(jī)制的神秘面紗箱歧。
2. 可擴(kuò)展的幾種解決方案
通常可擴(kuò)展的實(shí)現(xiàn)有下面幾種:
Factory模式
IoC容器
OSGI容器
Dubbo作為一個(gè)框架一膨,不希望強(qiáng)依賴(lài)其他的IoC容器呀邢,比如Spring,Guice豹绪。OSGI也是一個(gè)很重的實(shí)現(xiàn)价淌,不適合Dubbo。最終Dubbo的實(shí)現(xiàn)參考了Java原生的SPI機(jī)制瞒津,但對(duì)其進(jìn)行了一些擴(kuò)展蝉衣,以滿(mǎn)足Dubbo的需求。
3. Java SPI機(jī)制
既然Dubbo的擴(kuò)展機(jī)制是基于Java原生的SPI機(jī)制巷蚪,那么我們就先來(lái)了解下Java SPI吧病毡。了解了Java的SPI,也就是對(duì)Dubbo的擴(kuò)展機(jī)制有一個(gè)基本的了解屁柏。如果對(duì)Java SPI比較了解的同學(xué)啦膜,可以跳過(guò)有送。
Java SPI(Service Provider Interface)是JDK內(nèi)置的一種動(dòng)態(tài)加載擴(kuò)展點(diǎn)的實(shí)現(xiàn)。在ClassPath的META-INF/services目錄下放置一個(gè)與接口同名的文本文件功戚,文件的內(nèi)容為接口的實(shí)現(xiàn)類(lèi)娶眷,多個(gè)實(shí)現(xiàn)類(lèi)用換行符分隔似嗤。JDK中使用java.util.ServiceLoader來(lái)加載具體的實(shí)現(xiàn)啸臀。 讓我們通過(guò)一個(gè)簡(jiǎn)單的例子,來(lái)看看Java SPI是如何工作的烁落。
定義一個(gè)接口IRepository用于實(shí)現(xiàn)數(shù)據(jù)儲(chǔ)存
interface IRepository { void save(String data); }
提供IRepository的實(shí)現(xiàn) IRepository有兩個(gè)實(shí)現(xiàn)乘粒。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目錄添加一個(gè)文件伤塌,文件名和接口全名稱(chēng)相同灯萍,所以文件是META-INF/services/com.demo.IRepository。文件內(nèi)容為:
com.demo.MongoRepository com.demo.MysqlRepository
通過(guò)ServiceLoader加載IRepository實(shí)現(xiàn)
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"); }
在上面的例子中每聪,我們定義了一個(gè)擴(kuò)展點(diǎn)和它的兩個(gè)實(shí)現(xiàn)旦棉。在ClassPath中添加了擴(kuò)展的配置文件,最后使用ServiceLoader來(lái)加載所有的擴(kuò)展點(diǎn)药薯。
4. Dubbo的SPI機(jī)制
Java SPI的使用很簡(jiǎn)單绑洛。也做到了基本的加載擴(kuò)展點(diǎn)的功能。但Java SPI有以下的不足:
需要遍歷所有的實(shí)現(xiàn)童本,并實(shí)例化真屯,然后我們?cè)谘h(huán)中才能找到我們需要的實(shí)現(xiàn)。
配置文件中只是簡(jiǎn)單的列出了所有的擴(kuò)展實(shí)現(xiàn)穷娱,而沒(méi)有給他們命名绑蔫。導(dǎo)致在程序中很難去準(zhǔn)確的引用它們。
擴(kuò)展如果依賴(lài)其他的擴(kuò)展泵额,做不到自動(dòng)注入和裝配
不提供類(lèi)似于Spring的AOP功能
擴(kuò)展很難和其他的框架集成配深,比如擴(kuò)展里面依賴(lài)了一個(gè)Spring bean,原生的Java SPI不支持
所以Java SPI應(yīng)付一些簡(jiǎn)單的場(chǎng)景是可以的嫁盲,但對(duì)于Dubbo篓叶,它的功能還是比較弱的。Dubbo對(duì)原生SPI機(jī)制進(jìn)行了一些擴(kuò)展亡资。接下來(lái)澜共,我們就更深入地了解下Dubbo的SPI機(jī)制。
5. Dubbo擴(kuò)展點(diǎn)機(jī)制基本概念
在深入學(xué)習(xí)Dubbo的擴(kuò)展機(jī)制之前锥腻,我們先明確Dubbo SPI中的一些基本概念嗦董。在接下來(lái)的內(nèi)容中,我們會(huì)多次用到這些術(shù)語(yǔ)瘦黑。
擴(kuò)展點(diǎn)(Extension Point)
是一個(gè)Java的接口京革。
擴(kuò)展(Extension)
擴(kuò)展點(diǎn)的實(shí)現(xiàn)類(lèi)奇唤。
擴(kuò)展實(shí)例(Extension Instance)
擴(kuò)展點(diǎn)實(shí)現(xiàn)類(lèi)的實(shí)例。
擴(kuò)展自適應(yīng)實(shí)例(Extension Adaptive Instance)
第一次接觸這個(gè)概念時(shí)匹摇,可能不太好理解(我第一次也是這樣的...)咬扇。如果稱(chēng)它為擴(kuò)展代理類(lèi),可能更好理解些廊勃。擴(kuò)展的自適應(yīng)實(shí)例其實(shí)就是一個(gè)Extension的代理懈贺,它實(shí)現(xiàn)了擴(kuò)展點(diǎn)接口。在調(diào)用擴(kuò)展點(diǎn)的接口方法時(shí)坡垫,會(huì)根據(jù)實(shí)際的參數(shù)來(lái)決定要使用哪個(gè)擴(kuò)展梭灿。比如一個(gè)IRepository的擴(kuò)展點(diǎn),有一個(gè)save方法冰悠。有兩個(gè)實(shí)現(xiàn)MysqlRepository和MongoRepository堡妒。IRepository的自適應(yīng)實(shí)例在調(diào)用接口方法的時(shí)候,會(huì)根據(jù)save方法中的參數(shù)溉卓,來(lái)決定要調(diào)用哪個(gè)IRepository的實(shí)現(xiàn)皮迟。如果方法參數(shù)中有repository=mysql,那么就調(diào)用MysqlRepository的save方法桑寨。如果repository=mongo伏尼,就調(diào)用MongoRepository的save方法。和面向?qū)ο蟮难舆t綁定很類(lèi)似西疤。為什么Dubbo會(huì)引入擴(kuò)展自適應(yīng)實(shí)例的概念呢烦粒?
Dubbo中的配置有兩種,一種是固定的系統(tǒng)級(jí)別的配置代赁,在Dubbo啟動(dòng)之后就不會(huì)再改了扰她。還有一種是運(yùn)行時(shí)的配置,可能對(duì)于每一次的RPC芭碍,這些配置都不同徒役。比如在xml文件中配置了超時(shí)時(shí)間是10秒鐘,這個(gè)配置在Dubbo啟動(dòng)之后窖壕,就不會(huì)改變了忧勿。但針對(duì)某一次的RPC調(diào)用,可以設(shè)置它的超時(shí)時(shí)間是30秒鐘瞻讽,以覆蓋系統(tǒng)級(jí)別的配置鸳吸。對(duì)于Dubbo而言,每一次的RPC調(diào)用的參數(shù)都是未知的速勇。只有在運(yùn)行時(shí)晌砾,根據(jù)這些參數(shù)才能做出正確的決定。
很多時(shí)候烦磁,我們的類(lèi)都是一個(gè)單例的养匈,比如Spring的bean哼勇,在Spring bean都實(shí)例化時(shí),如果它依賴(lài)某個(gè)擴(kuò)展點(diǎn)呕乎,但是在bean實(shí)例化時(shí)积担,是不知道究竟該使用哪個(gè)具體的擴(kuò)展實(shí)現(xiàn)的。這時(shí)候就需要一個(gè)代理模式了猬仁,它實(shí)現(xiàn)了擴(kuò)展點(diǎn)接口帝璧,方法內(nèi)部可以根據(jù)運(yùn)行時(shí)參數(shù),動(dòng)態(tài)的選擇合適的擴(kuò)展實(shí)現(xiàn)逐虚。而這個(gè)代理就是自適應(yīng)實(shí)例聋溜。 自適應(yīng)擴(kuò)展實(shí)例在Dubbo中的使用非常廣泛谆膳,Dubbo中叭爱,每一個(gè)擴(kuò)展都會(huì)有一個(gè)自適應(yīng)類(lèi),如果我們沒(méi)有提供漱病,Dubbo會(huì)使用字節(jié)碼工具為我們自動(dòng)生成一個(gè)买雾。所以我們基本感覺(jué)不到自適應(yīng)類(lèi)的存在。后面會(huì)有例子說(shuō)明自適應(yīng)類(lèi)是怎么工作的杨帽。
@SPI
@SPI注解作用于擴(kuò)展點(diǎn)的接口上漓穿,表明該接口是一個(gè)擴(kuò)展點(diǎn)∽⒂可以被Dubbo的ExtentionLoader加載晃危。如果沒(méi)有此ExtensionLoader調(diào)用會(huì)異常。
@Adaptive
@Adaptive注解用在擴(kuò)展接口的方法上老客。表示該方法是一個(gè)自適應(yīng)方法僚饭。Dubbo在為擴(kuò)展點(diǎn)生成自適應(yīng)實(shí)例時(shí),如果方法有@Adaptive注解胧砰,會(huì)為該方法生成對(duì)應(yīng)的代碼鳍鸵。方法內(nèi)部會(huì)根據(jù)方法的參數(shù),來(lái)決定使用哪個(gè)擴(kuò)展尉间。
ExtentionLoader
類(lèi)似于Java SPI的ServiceLoader偿乖,負(fù)責(zé)擴(kuò)展的加載和生命周期維護(hù)。
擴(kuò)展別名
和Java SPI不同哲嘲,Dubbo中的擴(kuò)展都有一個(gè)別名贪薪,用于在應(yīng)用中引用它們。比如
random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
其中的random眠副,roundrobin就是對(duì)應(yīng)擴(kuò)展的別名画切。這樣我們?cè)谂渲梦募惺褂胷andom或roundrobin就可以了。
一些路徑
和Java SPI從/META-INF/services目錄加載擴(kuò)展配置類(lèi)似侦啸,Dubbo也會(huì)從以下路徑去加載擴(kuò)展配置文件:
META-INF/dubbo/internal
META-INF/dubbo
META-INF/services
6. Dubbo的LoadBalance擴(kuò)展點(diǎn)解讀
在了解了Dubbo的一些基本概念后槽唾,讓我們一起來(lái)看一個(gè)Dubbo中實(shí)際的擴(kuò)展點(diǎn)丧枪,對(duì)這些概念有一個(gè)更直觀的認(rèn)識(shí)。
我們選擇的是Dubbo中的LoadBalance擴(kuò)展點(diǎn)庞萍。Dubbo中的一個(gè)服務(wù)拧烦,通常有多個(gè)Provider,consumer調(diào)用服務(wù)時(shí)钝计,需要在多個(gè)Provider中選擇一個(gè)恋博。這就是一個(gè)LoadBalance。我們一起來(lái)看看在Dubbo中私恬,LoadBalance是如何成為一個(gè)擴(kuò)展點(diǎn)的债沮。
LoadBalance接口
@SPI(RandomLoadBalance.NAME) public interface LoadBalance { @Adaptive("loadbalance") Invoker select(List> invokers, URL url, Invocation invocation) throws RpcException; }
LoadBalance接口只有一個(gè)select方法。select方法從多個(gè)invoker中選擇其中一個(gè)本鸣。上面代碼中和Dubbo SPI相關(guān)的元素有:
@SPI(RandomLoadBalance.NAME) @SPI作用于LoadBalance接口疫衩,表示接口LoadBalance是一個(gè)擴(kuò)展點(diǎn)。如果沒(méi)有@SPI注解荣德,試圖去加載擴(kuò)展時(shí)闷煤,會(huì)拋出異常。@SPI注解有一個(gè)參數(shù)涮瞻,該參數(shù)表示該擴(kuò)展點(diǎn)的默認(rèn)實(shí)現(xiàn)的別名鲤拿。如果沒(méi)有顯示的指定擴(kuò)展,就使用默認(rèn)實(shí)現(xiàn)署咽。RandomLoadBalance.NAME是一個(gè)常量近顷,值是"random",是一個(gè)隨機(jī)負(fù)載均衡的實(shí)現(xiàn)宁否。 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個(gè)LoadBalance的擴(kuò)展實(shí)現(xiàn)窒升。由于負(fù)載均衡的實(shí)現(xiàn)不是本次的內(nèi)容,這里就不過(guò)多說(shuō)明家淤。只用知道Dubbo提供了4種負(fù)載均衡的實(shí)現(xiàn)异剥,我們可以通過(guò)xml文件,properties文件絮重,JVM參數(shù)顯式的指定一個(gè)實(shí)現(xiàn)冤寿。如果沒(méi)有,默認(rèn)使用隨機(jī)青伤。
@Adaptive("loadbalance") @Adaptive注解修飾select方法督怜,表明方法select方法是一個(gè)可自適應(yīng)的方法。Dubbo會(huì)自動(dòng)生成該方法對(duì)應(yīng)的代碼狠角。當(dāng)調(diào)用select方法時(shí)号杠,會(huì)根據(jù)具體的方法參數(shù)來(lái)決定調(diào)用哪個(gè)擴(kuò)展實(shí)現(xiàn)的select方法。@Adaptive注解的參數(shù)loadbalance表示方法參數(shù)中的loadbalance的值作為實(shí)際要調(diào)用的擴(kuò)展實(shí)例。 但奇怪的是姨蟋,我們發(fā)現(xiàn)select的方法中并沒(méi)有l(wèi)oadbalance參數(shù)屉凯,那怎么獲取loadbalance的值呢?select方法中還有一個(gè)URL類(lèi)型的參數(shù)眼溶,Dubbo就是從URL中獲取loadbalance的值的悠砚。這里涉及到Dubbo的URL總線模式,簡(jiǎn)單說(shuō)堂飞,URL中包含了RPC調(diào)用中的所有參數(shù)灌旧。URL類(lèi)中有一個(gè)Map parameters字段,parameters中就包含了loadbalance绰筛。
獲取LoadBalance擴(kuò)展
Dubbo中獲取LoadBalance的代碼如下:
LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName);
使用ExtensionLoader.getExtensionLoader(LoadBalance.class)方法獲取一個(gè)ExtensionLoader的實(shí)例枢泰,然后調(diào)用getExtension,傳入一個(gè)擴(kuò)展的別名來(lái)獲取對(duì)應(yīng)的擴(kuò)展實(shí)例铝噩。
7. 自定義一個(gè)LoadBalance擴(kuò)展
本節(jié)中衡蚂,我們通過(guò)一個(gè)簡(jiǎn)單的例子,來(lái)自己實(shí)現(xiàn)一個(gè)LoadBalance薄榛,并把它集成到Dubbo中讳窟。我會(huì)列出一些關(guān)鍵的步驟和代碼,也可以從這個(gè)地址(https://github.com/vangoleo/dubbo-spi-demo)下載完整的demo敞恋。
實(shí)現(xiàn)LoadBalance接口
首先,編寫(xiě)一個(gè)自己實(shí)現(xiàn)的LoadBalance谋右,因?yàn)槭菫榱搜菔綝ubbo的擴(kuò)展機(jī)制硬猫,而不是LoadBalance的實(shí)現(xiàn),所以這里L(fēng)oadBalance的實(shí)現(xiàn)非常簡(jiǎn)單改执,選擇第一個(gè)invoker啸蜜,并在控制臺(tái)輸出一條日志。
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); } }
添加擴(kuò)展配置文件
添加文件:META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance辈挂。文件內(nèi)容如下:
demo=com.dubbo.spi.demo.consumer.DemoLoadBalance
配置使用自定義LoadBalance
通過(guò)上面的兩步衬横,已經(jīng)添加了一個(gè)名字為demo的LoadBalance實(shí)現(xiàn),并在配置文件中進(jìn)行了相應(yīng)的配置终蒂。接下來(lái)蜂林,需要顯式的告訴Dubbo使用demo的負(fù)載均衡實(shí)現(xiàn)。如果是通過(guò)spring的方式使用Dubbo拇泣,可以在xml文件中進(jìn)行設(shè)置噪叙。
在consumer端的dubbo:reference中配置
啟動(dòng)Dubbo
啟動(dòng)Dubbo,調(diào)用一次IHelloService霉翔,可以看到控制臺(tái)會(huì)輸出一條DemoLoadBalance: Select the first invoker...日志睁蕾。說(shuō)明Dubbo的確是使用了我們自定義的LoadBalance。
總結(jié)
到此,我們從Java SPI開(kāi)始子眶,了解了Dubbo SPI 的基本概念瀑凝,并結(jié)合了Dubbo中的LoadBalance加深了理解。最后臭杰,我們還實(shí)踐了一下猜丹,創(chuàng)建了一個(gè)自定義LoadBalance,并集成到Dubbo中硅卢。相信通過(guò)這里理論和實(shí)踐的結(jié)合射窒,大家對(duì)Dubbo的可擴(kuò)展有更深入的理解。
總結(jié)一下将塑,Dubbo SPI有以下的特點(diǎn):
? 對(duì)Dubbo進(jìn)行擴(kuò)展脉顿,不需要改動(dòng)Dubbo的源碼
? 自定義的Dubbo的擴(kuò)展點(diǎn)實(shí)現(xiàn),是一個(gè)普通的Java類(lèi)点寥,Dubbo沒(méi)有引入任何Dubbo特有的元素艾疟,對(duì)代碼侵入性幾乎為零。
? 將擴(kuò)展注冊(cè)到Dubbo中敢辩,只需要在ClassPath中添加配置文件蔽莱。使用簡(jiǎn)單。而且不會(huì)對(duì)現(xiàn)有代碼造成影響戚长。符合開(kāi)閉原則盗冷。
? Dubbo的擴(kuò)展機(jī)制支持IoC,AoP等高級(jí)功能
? Dubbo的擴(kuò)展機(jī)制能很好的支持第三方IoC容器,默認(rèn)支持Spring Bean同廉,可自己擴(kuò)展來(lái)支持其他容器仪糖,比如Google的Guice。
? 切換擴(kuò)展點(diǎn)的實(shí)現(xiàn)迫肖,只需要在配置文件中修改具體的實(shí)現(xiàn)锅劝,不需要改代碼。使用方便蟆湖。