翻看
Dubbo
的源碼奠伪,不難發(fā)現(xiàn)烧颖,框架到處都在用SPI
機制進行擴展。這是由于Dubbo
框架對各種層做了很多的實現(xiàn)方式艺普,然后由用戶自己去選擇具體的實現(xiàn)方式。比如Protocol
鉴竭,Dubbo
提供的實現(xiàn)就有dubbo
歧譬,hessian
,thrift
搏存,redis
瑰步,inJvm
等等。這么多的實現(xiàn)方式璧眠,Dubbo
是如何做到動態(tài)的切換的呢缩焦?
接口自適應與 @Adaptive 注解
Dubbo
沒有采用JDK
提供的SPI
機制,而是自己實現(xiàn)了一套责静,增強了很多功能袁滥。具體細節(jié)可以瀏覽網(wǎng)上其他博客。
Dubbo
充分利用面向對象思想泰演,每一個組件內(nèi)引入其他組件都是以接口的形式進行依賴呻拌,動態(tài)的 inject 實現(xiàn)類。所以這種思想上也用到了 AOP
和 DI
的思想睦焕。
我們定義一個接口 Registry
藐握,假定它的功能是將本地服務暴露到注冊中心,以及從注冊中心獲取可用服務垃喊,屬于 RPC
框架中的服務注冊與發(fā)現(xiàn)組件猾普。
@SPI("zookeeper")
public interface Registry {
/**
* 注冊服務
*/
@Adaptive()
String register(URL url, String content);
/**
* 發(fā)現(xiàn)服務
*/
@Adaptive()
String discovery(URL url, String content);
}
-
@SPI
注解標注這是一個可擴展的組件,注解內(nèi)的內(nèi)容后文詳細介紹本谜。 - 該接口定義了兩個方法
register
和discovery
初家,分別代表注冊服務和發(fā)現(xiàn)服務。兩個方法中都有URL
參數(shù)乌助,該類是Dubbo
內(nèi)置的類溜在,代表了Dubbo
整個執(zhí)行過程中的上下文信息,包括各類配置信息他托,參數(shù)等掖肋。content
代表備注信息。 -
@Adaptive()
注解表明這兩個方法都是自適應方法赏参,具體作用后文分析志笼。
下面分別是兩個實現(xiàn)類 ZookeeperRegistry
、EtcdRegistry
ZookeeperRegistry
public class ZookeeperRegistry implements Registry {
private Logger logger = LoggerFactory.getLogger(ZookeeperRegistry.class);
@Override
public String register(URL url, String content) {
logger.info("服務: {} 已注冊到zookeeper上把篓,備注: {}", url.getParameter("service"), content);
return "Zookeeper register already! ";
}
@Override
public String discovery(URL url, String content) {
logger.info("zookeeper上發(fā)現(xiàn)服務: {} , 備注: {}", url.getParameter("service"), content);
return "Zookeeper discovery already! ";
}
}
EtcdRegistry
public class EtcdRegistry implements Registry {
private Logger logger = LoggerFactory.getLogger(ZookeeperRegistry.class);
@Override
public String register(URL url, String content) {
logger.info("服務: {} 已注冊到 Etcd 上纫溃,備注: {}", url.getParameter("service"), content);
return "Etcd register already! ";
}
@Override
public String discovery(URL url, String content) {
logger.info("Etcd 上發(fā)現(xiàn)服務: {} , 備注: {}", url.getParameter("service"), content);
return "Etcd discovery already! ";
}
}
我們可以將服務注冊信息注冊到老牌注冊中心 zookeeper
上,或者使用新興流行輕量級注冊中心 etcd
上韧掩。
配置擴展點實現(xiàn)信息到 Resource
目錄下
在 Resource
下的 META-INF.dubbo
下新建 以Registry
全限定名為名的文件紊浩,配置實現(xiàn)類信息,以 key-value
的形式疗锐。(這里要注意與 JDK
默認的 SPI
機制的區(qū)別)
文件內(nèi)內(nèi)容如下:
etcd=com.maple.spi.impl.EtcdRegistry
zookeeper=com.maple.spi.impl.ZookeeperRegistry
測試 Dubbo SPI
自適應類
public class Main {
public static void main(String[] args) {
URL url = URL.valueOf("test://localhost/test")
.addParameter("service", "helloService");
Registry registry = ExtensionLoader.getExtensionLoader(Registry.class)
.getAdaptiveExtension();
String register = registry.register(url, "maple");
System.out.println(register);
}
}
該程序首先通過 Registry
接口得到它專屬的 ExtensionLoader
實例郎楼,然后調(diào)用 getAdaptiveExtension
拿到該接口的自適應類。Dubbo
會判斷是否有實現(xiàn)類(即實現(xiàn)了 Registry
接口) 上有注解 @Adaptive
窒悔,如果沒有就會動態(tài)生成呜袁。本例子將會動態(tài)生成。
直接運行程序結果如下,程序最終選擇的實現(xiàn)類是 ZookeeperRegistry
简珠,控制臺結果如下:
09-20 23:43:13 323 main INFO - 服務: helloService 已注冊到zookeeper上阶界,備注: maple
Zookeeper register already!
上面代碼我們沒有看到任何實現(xiàn)類的信息,Dubbo SPI
機制會為動態(tài)的去調(diào)用實現(xiàn)類聋庵。
我們重點分析 getAdaptiveExtension
方法找到的是 Registry
的自適應類膘融,可以理解為是 Registry
的一個 適配器和代理類。如果該適配器類不存在祭玉,Dubbo
會通過動態(tài)代理方式在運行時自動生成一個自適應類氧映。
打開 DEBUG
日志,在控制臺我們看到了 Dubbo
生成的類的源碼如下:
package com.maple.spi;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Registry$Adaptive implements com.maple.spi.Registry {
public java.lang.String register(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg0;
String extName = url.getParameter("registry", "zookeeper");
if (extName == null)
throw new IllegalStateException("Fail to get extension(com.maple.spi.Registry) name from url(" + url.toString() + ") use keys([registry])");
com.maple.spi.Registry extension = (com.maple.spi.Registry) ExtensionLoader.getExtensionLoader(com.maple.spi.Registry.class).getExtension(extName);
return extension.register(arg0, arg1);
}
public java.lang.String discovery(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg0;
String extName = url.getParameter("registry", "zookeeper");
if (extName == null)
throw new IllegalStateException("Fail to get extension(com.maple.spi.Registry) name from url(" + url.toString() + ") use keys([registry])");
com.maple.spi.Registry extension = (com.maple.spi.Registry) ExtensionLoader.getExtensionLoader(com.maple.spi.Registry.class).getExtension(extName);
return extension.discovery(arg0, arg1);
}
}
看代碼脱货,該適配器類的作用是類似于 AOP
的功能岛都,再調(diào)用具體的實現(xiàn)類之前律姨,先通ExtensionLoader.getExtensionLoader(Registry.class).getExtension(extName);
根據(jù) extName
去 loader
具體的實現(xiàn)類,然后再去調(diào)用實現(xiàn)類的相應的方法臼疫。
分析上面代碼中的一句:
String extName = url.getParameter("registry", "zookeeper");
extName
可以通過 url
進行傳遞择份,默認值為 zookeeper
, 該默認值即為我們定義的接口上的注解 @SPI
里的內(nèi)容,上文我們定義的 @SPI("zookeeper")
烫堤,所以這里的默認值為 zookeeper
, 當 url
中沒有對應的參數(shù)時荣赶,我們會去拿默認值。
我們可以修改 Main
測試程序鸽斟,增加 key
為 registry
的 parameter
public static void main(String[] args) {
URL url = URL.valueOf("test://localhost/test").addParameter("service", "helloService")
.addParameter("registry","etcd");
Registry registry = ExtensionLoader.getExtensionLoader(Registry.class).getAdaptiveExtension();
String register = registry.register(url, "maple");
System.out.println(register);
}
在 URL
中增加 Key
拔创,并設置值為 etcd
,運行程序富蓄,結果如下:
09-20 23:44:00 009 main INFO - 服務: helloService 已注冊到 Etcd 上剩燥,備注: maple
Etcd register already!
實現(xiàn)類已經(jīng)切換為 EtcdRegistry
了。
@Adaptive 注意細節(jié)
@Adaptive 源碼
public @interface Adaptive {
String[] value() default {};
}
細心的讀者已經(jīng)發(fā)現(xiàn)了 @Adaptive
有value
這個屬性格粪。上文在接口方法上定義的 @Adaptive
是沒有設置值的躏吊。如果沒有定義值,Dubbo
默認會使用一種策略生成帐萎。這種策略是將類名定義的駝峰法則轉換為小寫比伏,并以 .
號區(qū)分。 例如上文的接口名為 Registry
疆导,那么這個Key
值就是 registry
赁项。如果接口名為 HelloWorld
,Key
值就為 hello.world
澈段。
當然如果 @Adaptive
是有值的話悠菜,優(yōu)先按里面的這個值來作為 Key
,例如 Dubbo
框架中的接口 RegistryFactory
,該接口的自適應類將會從 URL
以 protocol
為 key
來找實現(xiàn)類的 extName
败富。
@SPI("dubbo")
public interface RegistryFactory {
@Adaptive({"protocol"})
Registry getRegistry(URL url);
}
總結
Dubbo Adaptive
模式在整個框架中運用十分廣泛悔醋,如果用戶沒有在 URL
中進行自定義,Dubbo
默認會去加載擴展點接口上 @SPI
標注的內(nèi)容,如果此注解沒有值兽叮,那么我們就必須要在 URL
中進行值的傳遞了芬骄。如果我們想覆蓋 Dubbo
默認的實現(xiàn)策略○写希可以通過在 URL
中增加 key-value
的形式來改變账阻。
這樣一個組件充分體現(xiàn)了設計模式,對修改關閉,對擴展開放的原則泽本。當我們想自己實現(xiàn) Dubbo
中的某個組件時淘太,我們完全可以通過 Dubbo Adaptive
來動態(tài)的切換程序使用我們提供的組件。
徹底理解 Dubbo SPI
模式,以及 Adaptive
自適應類, Activate
激活類等蒲牧,是看 Dubbo
源碼的基礎撇贺。如果我們想十分流暢的去分析 Dubbo
內(nèi)部其他組件的實現(xiàn)機制,第一道要跨過的坎便是 Dubbo SPI
造成。