更新時(shí)間:2022-09-15 來源:黑馬程序員 瀏覽量:
架構(gòu)體系
框架介紹
概述
Dubbo是阿里巴巴公司開源的一個(gè)高性能優(yōu)秀的服務(wù)框架,使得應(yīng)用可通過高性能的 RPC 實(shí)現(xiàn)服務(wù)的輸出和輸入功能,可以和 Spring框架無縫集成。
Dubbo是一款高性能、輕量級(jí)的開源Java RPC框架,它提供了三大核心能力:面向接口的遠(yuǎn)程方法調(diào)用,智能容錯(cuò)和負(fù)載均衡,以及服務(wù)自動(dòng)注冊(cè)和發(fā)現(xiàn)。
相關(guān)概念
dubbo運(yùn)行架構(gòu)如下圖示
節(jié)點(diǎn)角色說明
| 節(jié)點(diǎn) | 角色說明 |
| ----------- | -------------------------------------- |
| `Provider` | 暴露服務(wù)的服務(wù)提供方 |
| `Consumer` | 調(diào)用遠(yuǎn)程服務(wù)的服務(wù)消費(fèi)方 |
| `Registry` | 服務(wù)注冊(cè)與發(fā)現(xiàn)的注冊(cè)中心 |
| `Monitor` | 統(tǒng)計(jì)服務(wù)的調(diào)用次數(shù)和調(diào)用時(shí)間的監(jiān)控中心 |
| `Container` | 服務(wù)運(yùn)行容器 |
調(diào)用關(guān)系說明
1. 服務(wù)容器負(fù)責(zé)啟動(dòng),加載,運(yùn)行服務(wù)提供者。
2. 服務(wù)提供者在啟動(dòng)時(shí),向注冊(cè)中心注冊(cè)自己提供的服務(wù)。
3. 服務(wù)消費(fèi)者在啟動(dòng)時(shí),向注冊(cè)中心訂閱自己所需的服務(wù)。
4. 注冊(cè)中心返回服務(wù)提供者地址列表給消費(fèi)者,如果有變更,注冊(cè)中心將基于長(zhǎng)連接推送變更數(shù)據(jù)給消費(fèi)者。
5. 服務(wù)消費(fèi)者,從提供者地址列表中,基于軟負(fù)載均衡算法,選一臺(tái)提供者進(jìn)行調(diào)用,如果調(diào)用失敗,再選另一臺(tái)調(diào)用。
6. 服務(wù)消費(fèi)者和提供者,在內(nèi)存中累計(jì)調(diào)用次數(shù)和調(diào)用時(shí)間,定時(shí)每分鐘發(fā)送一次統(tǒng)計(jì)數(shù)據(jù)到監(jiān)控中心。
關(guān)于dubbo 的特點(diǎn)分別有連通性、健壯性、伸縮性、以及向未來架構(gòu)的升級(jí)性。特點(diǎn)的詳細(xì)介紹也可以參考[官方文檔](http://dubbo.apache.org/zh-cn/docs/user/preface/architecture.html)。
環(huán)境搭建
接下來逐步對(duì)dubbo各個(gè)模塊的源碼以及原理進(jìn)行解析,目前dubbo框架已經(jīng)交由Apache基金會(huì)進(jìn)行孵化,被在github開源。
Dubbo 社區(qū)目前主力維護(hù)的有 2.6.x 和 2.7.x 兩大版本,其中,
- 2.6.x 主要以 bugfix 和少量 enhancements 為主,因此能完全保證穩(wěn)定性
- 2.7.x 作為社區(qū)的主要開發(fā)版本,得到持續(xù)更新并增加了大量新 feature 和優(yōu)化,同時(shí)也帶來了一些穩(wěn)定性挑戰(zhàn)
源碼拉取
通過以下的這個(gè)命令簽出最新的dubbo項(xiàng)目源碼,并導(dǎo)入到IDEA中
git clone https://github.com/apache/dubbo.git dubbo
可以看到Dubbo被拆分成很多的Maven項(xiàng)目,在后續(xù)課程中會(huì)介紹左邊每個(gè)模塊的大致作用。
環(huán)境導(dǎo)入
在本次課程中,不僅講解dubbo源碼還會(huì)涉及到相關(guān)的基礎(chǔ)知識(shí),為了方便學(xué)員快速理解并掌握各個(gè)內(nèi)容,已經(jīng)準(zhǔn)備好了相關(guān)工程,只需導(dǎo)入到IDEA中即可。對(duì)于工程中代碼的具體作用,在后續(xù)課程會(huì)依次講解。
測(cè)試
(1) 安裝zookeeper
(2) 修改官網(wǎng)案例,配置zookeeper地址
(3) 啟動(dòng)服務(wù)提供者,啟動(dòng)服務(wù)消費(fèi)者
架構(gòu)體系
源碼結(jié)構(gòu)
通過如下圖形可以大致的了解到,dubbo源碼各個(gè)模塊的相關(guān)作用:
模塊說明:
- dubbo-common 公共邏輯模塊:包括 Util 類和通用模型。
- dubbo-remoting 遠(yuǎn)程通訊模塊:相當(dāng)于 Dubbo 協(xié)議的實(shí)現(xiàn),如果 RPC 用 RMI協(xié)議則不需要使用此包。
- dubbo-rpc 遠(yuǎn)程調(diào)用模塊:抽象各種協(xié)議,以及動(dòng)態(tài)代理,只包含一對(duì)一的調(diào)用,不關(guān)心集群的管理。
- dubbo-cluster 集群模塊:將多個(gè)服務(wù)提供方偽裝為一個(gè)提供方,包括:負(fù)載均衡, 容錯(cuò),路由等,集群的地址列表可以是靜態(tài)配置的,也可以是由注冊(cè)中心下發(fā)。
- dubbo-registry 注冊(cè)中心模塊:基于注冊(cè)中心下發(fā)地址的集群方式,以及對(duì)各種注冊(cè)中心的抽象。
- dubbo-monitor 監(jiān)控模塊:統(tǒng)計(jì)服務(wù)調(diào)用次數(shù),調(diào)用時(shí)間的,調(diào)用鏈跟蹤的服務(wù)。
- dubbo-config 配置模塊:是 Dubbo 對(duì)外的 API,用戶通過 Config 使用Dubbo,隱藏 Dubbo 所有細(xì)節(jié)。
- dubbo-container 容器模塊:是一個(gè) Standlone 的容器,以簡(jiǎn)單的 Main 加載 Spring 啟動(dòng),因?yàn)榉?wù)通常不需要 Tomcat/JBoss 等 Web 容器的特性,沒必要用 Web 容器去加載服務(wù)。
整體設(shè)計(jì)
圖例說明:
- 圖中左邊淡藍(lán)背景的為服務(wù)消費(fèi)方使用的接口,右邊淡綠色背景的為服務(wù)提供方使用的接口,位于中軸線上的為雙方都用到的接口。
- 圖中從下至上分為十層,各層均為單向依賴,右邊的黑色箭頭代表層之間的依賴關(guān)系,每一層都可以剝離上層被復(fù)用,其中,Service 和 Config 層為 API,其它各層均為 SPI。
- 圖中綠色小塊的為擴(kuò)展接口,藍(lán)色小塊為實(shí)現(xiàn)類,圖中只顯示用于關(guān)聯(lián)各層的實(shí)現(xiàn)類。
- 圖中藍(lán)色虛線為初始化過程,即啟動(dòng)時(shí)組裝鏈,紅色實(shí)線為方法調(diào)用過程,即運(yùn)行時(shí)調(diào)時(shí)鏈,紫色三角箭頭為繼承,可以把子類看作父類的同一個(gè)節(jié)點(diǎn),線上的文字為調(diào)用的方法。
各層說明
- config 配置層:對(duì)外配置接口,以 `ServiceConfig`, `ReferenceConfig` 為中心,可以直接初始化配置類,也可以通過 spring 解析配置生成配置類。
- proxy 服務(wù)代理層:服務(wù)接口透明代理,生成服務(wù)的客戶端 Stub 和服務(wù)器端 Skeleton, 以 `ServiceProxy` 為中心,擴(kuò)展接口為 `ProxyFactory`。
- registry 注冊(cè)中心層:封裝服務(wù)地址的注冊(cè)與發(fā)現(xiàn),以服務(wù) URL 為中心,擴(kuò)展接口為 `RegistryFactory`, `Registry`, `RegistryService`。
- cluster 路由層:封裝多個(gè)提供者的路由及負(fù)載均衡,并橋接注冊(cè)中心,以 `Invoker` 為中心,擴(kuò)展接口為 `Cluster`, `Directory`, `Router`, `LoadBalance`。
- monitor 監(jiān)控層:RPC 調(diào)用次數(shù)和調(diào)用時(shí)間監(jiān)控,以 `Statistics` 為中心,擴(kuò)展接口為 `MonitorFactory`, `Monitor`, `MonitorService`。
- protocol 遠(yuǎn)程調(diào)用層:封裝 RPC 調(diào)用,以 `Invocation`, `Result` 為中心,擴(kuò)展接口為 `Protocol`, `Invoker`, `Exporter`。
- exchange 信息交換層:封裝請(qǐng)求響應(yīng)模式,同步轉(zhuǎn)異步,以 `Request`, `Response` 為中心,擴(kuò)展接口為 `Exchanger`, `ExchangeChannel`, `ExchangeClient`, `ExchangeServer`。
- transport 網(wǎng)絡(luò)傳輸層:抽象 mina 和 netty 為統(tǒng)一接口,以 `Message` 為中心,擴(kuò)展接口為 `Channel`, `Transporter`, `Client`, `Server`, `Codec`。
- serialize 數(shù)據(jù)序列化層:可復(fù)用的一些工具,擴(kuò)展接口為 `Serialization`, `ObjectInput`, `ObjectOutput`, `ThreadPool`。
SPI機(jī)制
在 Dubbo 中,SPI 是一個(gè)非常重要的模塊?;?SPI,我們可以很容易的對(duì) Dubbo 進(jìn)行拓展。如果大家想要學(xué)習(xí) Dubbo 的源碼,SPI 機(jī)制務(wù)必弄懂。接下來,我們先來了解一下 Java SPI 與 Dubbo SPI 的用法,然后再來分析 Dubbo SPI 的源碼。
SPI的概述
SPI的主要作用
SPI 全稱為 Service Provider Interface,是一種服務(wù)發(fā)現(xiàn)機(jī)制。SPI 的本質(zhì)是將接口實(shí)現(xiàn)類的全限定名配置在文件中,并由服務(wù)加載器讀取配置文件,加載實(shí)現(xiàn)類。這樣可以在運(yùn)行時(shí),動(dòng)態(tài)為接口替換實(shí)現(xiàn)類。正因此特性,我們可以很容易的通過 SPI 機(jī)制為我們的程序提供拓展功能。
Java SPI 實(shí)際上是“基于接口的編程+策略模式+配置文件”組合實(shí)現(xiàn)的動(dòng)態(tài)加載機(jī)制。
入門案例
首先,我們定義一個(gè)接口,名稱為 Robot。
public interface Robot { void sayHello(); }
接下來定義兩個(gè)實(shí)現(xiàn)類,分別為 OptimusPrime 和 Bumblebee。
public class OptimusPrime implements Robot { @Override public void sayHello() { System.out.println("Hello, I am Optimus Prime."); } } public class Bumblebee implements Robot { @Override public void sayHello() { System.out.println("Hello, I am Bumblebee."); } }
接下來 META-INF/services 文件夾下創(chuàng)建一個(gè)文件,名稱為 Robot 的全限定名 com.itheima.java.spi.Robot。文件內(nèi)容為實(shí)現(xiàn)類的全限定的類名,如下:
```properties
com.itheima.java.spi.impl.Bumblebee
com.itheima.java.spi.impl.OptimusPrime
```
做好所需的準(zhǔn)備工作,接下來編寫代碼進(jìn)行測(cè)試。
public class JavaSPITest { @Test public void sayHello() throws Exception { ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class); System.out.println("Java SPI"); serviceLoader.forEach(Robot::sayHello); } }
最后來看一下測(cè)試結(jié)果,如下:
從測(cè)試結(jié)果可以看出,我們的兩個(gè)實(shí)現(xiàn)類被成功的加載,并輸出了相應(yīng)的內(nèi)容。
總結(jié)
調(diào)用過程
應(yīng)用程序調(diào)用ServiceLoader.load方法,創(chuàng)建一個(gè)新的ServiceLoader,并實(shí)例化該類中的成員變量
應(yīng)用程序通過迭代器接口獲取對(duì)象實(shí)例,ServiceLoader先判斷成員變量providers對(duì)象中(LinkedHashMap類型)是否有緩存實(shí)例對(duì)象,如果有緩存,直接返回。
如果沒有緩存,執(zhí)行類的裝載,
優(yōu)點(diǎn)
使用 Java SPI 機(jī)制的優(yōu)勢(shì)是實(shí)現(xiàn)解耦,使得接口的定義與具體業(yè)務(wù)實(shí)現(xiàn)分離,而不是耦合在一起。應(yīng)用進(jìn)程可以根據(jù)實(shí)際業(yè)務(wù)情況啟用或替換具體組件。
缺點(diǎn)
- 不能按需加載。雖然 ServiceLoader 做了延遲載入,但是基本只能通過遍歷全部獲取,也就是接口的實(shí)現(xiàn)類得全部載入并實(shí)例化一遍。如果你并不想用某些實(shí)現(xiàn)類,或者某些類實(shí)例化很耗時(shí),它也被載入并實(shí)例化了,這就造成了浪費(fèi)。
- 獲取某個(gè)實(shí)現(xiàn)類的方式不夠靈活,只能通過 Iterator 形式獲取,不能根據(jù)某個(gè)參數(shù)來獲取對(duì)應(yīng)的實(shí)現(xiàn)類。
- 多個(gè)并發(fā)多線程使用 ServiceLoader 類的實(shí)例是不安全的。
- 加載不到實(shí)現(xiàn)類時(shí)拋出并不是真正原因的異常,錯(cuò)誤很難定位。
Dubbo中的SPI
概述
Dubbo 并未使用 Java SPI,而是重新實(shí)現(xiàn)了一套功能更強(qiáng)的 SPI 機(jī)制。Dubbo SPI 的相關(guān)邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以加載指定的實(shí)現(xiàn)類。
入門案例
與 Java SPI 實(shí)現(xiàn)類配置不同,Dubbo SPI 是通過鍵值對(duì)的方式進(jìn)行配置,這樣我們可以按需加載指定的實(shí)現(xiàn)類。下面來演示 Dubbo SPI 的用法:
Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路徑下,與 Java SPI 實(shí)現(xiàn)類配置不同,Dubbo SPI 是通過鍵值對(duì)的方式進(jìn)行配置,配置內(nèi)容如下。
```properties
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
```
在使用Dubbo SPI 時(shí),需要在接口上標(biāo)注 @SPI 注解。
@SPI public interface Robot { void sayHello(); }
通過 ExtensionLoader,我們可以加載指定的實(shí)現(xiàn)類,下面來演示 Dubbo SPI :
public class DubboSPITest { @Test public void sayHello() throws Exception { ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class); Robot optimusPrime = extensionLoader.getExtension("optimusPrime"); optimusPrime.sayHello(); Robot bumblebee = extensionLoader.getExtension("bumblebee"); bumblebee.sayHello(); } }
測(cè)試結(jié)果如下:
Dubbo SPI 除了支持按需加載接口實(shí)現(xiàn)類,還增加了 IOC 和 AOP 等特性,這些特性將會(huì)在接下來的源碼分析章節(jié)中一一進(jìn)行介紹。
源碼分析
上一章簡(jiǎn)單演示了 Dubbo SPI 的使用方法,首先通過 ExtensionLoader 的 getExtensionLoader 方法獲取一個(gè) ExtensionLoader 實(shí)例,然后再通過 ExtensionLoader 的 getExtension 方法獲取拓展類對(duì)象。下面我們從 ExtensionLoader 的 getExtension 方法作為入口,對(duì)拓展類對(duì)象的獲取過程進(jìn)行詳細(xì)的分析。
public T getExtension(String name) { if (StringUtils.isEmpty(name)) { throw new IllegalArgumentException("Extension name == null"); } if ("true".equals(name)) { // 獲取默認(rèn)的拓展實(shí)現(xiàn)類 return getDefaultExtension(); } // Holder,顧名思義,用于持有目標(biāo)對(duì)象 Holder<Object> holder = getOrCreateHolder(name); Object instance = holder.get(); // 雙重檢查 if (instance == null) { synchronized (holder) { instance = holder.get(); if (instance == null) { // 創(chuàng)建拓展實(shí)例 instance = createExtension(name); // 設(shè)置實(shí)例到 holder 中 holder.set(instance); } } } return (T) instance; }
上面代碼的邏輯比較簡(jiǎn)單,首先檢查緩存,緩存未命中則創(chuàng)建拓展對(duì)象。下面我們來看一下創(chuàng)建拓展對(duì)象的過程是怎樣的。
private T createExtension(String name) { // 從配置文件中加載所有的拓展類,可得到“配置項(xiàng)名稱”到“配置類”的映射關(guān)系表 Class<?> clazz = getExtensionClasses().get(name); if (clazz == null) { throw findException(name); } try { T instance = (T) EXTENSION_INSTANCES.get(clazz); if (instance == null) { // 通過反射創(chuàng)建實(shí)例 EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); instance = (T) EXTENSION_INSTANCES.get(clazz); } // 向?qū)嵗凶⑷胍蕾? injectExtension(instance); Set<Class<?>> wrapperClasses = cachedWrapperClasses; if (CollectionUtils.isNotEmpty(wrapperClasses)) { // 循環(huán)創(chuàng)建 Wrapper 實(shí)例 for (Class<?> wrapperClass : wrapperClasses) { // 將當(dāng)前 instance 作為參數(shù)傳給 Wrapper 的構(gòu)造方法,并通過反射創(chuàng)建 Wrapper 實(shí)例。 // 然后向 Wrapper 實(shí)例中注入依賴,最后將 Wrapper 實(shí)例再次賦值給 instance 變量 instance = injectExtension( (T) wrapperClass.getConstructor(type).newInstance(instance)); } } return instance; } catch (Throwable t) { throw new IllegalStateException("..."); } }
createExtension 方法的邏輯稍復(fù)雜一下,包含了如下的步驟:
1. 通過 getExtensionClasses 獲取所有的拓展類
2. 通過反射創(chuàng)建拓展對(duì)象
3. 向拓展對(duì)象中注入依賴
4. 將拓展對(duì)象包裹在相應(yīng)的 Wrapper 對(duì)象中
以上步驟中,第一個(gè)步驟是加載拓展類的關(guān)鍵,第三和第四個(gè)步驟是 Dubbo IOC 與 AOP 的具體實(shí)現(xiàn)。由于此類設(shè)計(jì)源碼較多,這里簡(jiǎn)單的總結(jié)下ExtensionLoader整個(gè)執(zhí)行邏輯:
```tex
getExtension(String name) #根據(jù)key獲取拓展對(duì)象
-->createExtension(String name) #創(chuàng)建拓展實(shí)例
-->getExtensionClasses #根據(jù)路徑獲取所有的拓展類
-->loadExtensionClasses #加載拓展類
-->cacheDefaultExtensionName #解析@SPI注解
-->loadDirectory #方法加載指定文件夾配置文件
-->loadResource #加載資源
-->loadClass #加載類,并通過 loadClass 方法對(duì)類進(jìn)行緩存
```
SPI中的IOC和AOP
依賴注入
Dubbo IOC 是通過 setter 方法注入依賴。Dubbo 首先會(huì)通過反射獲取到實(shí)例的所有方法,然后再遍歷方法列表,檢測(cè)方法名是否具有 setter 方法特征。若有,則通過 ObjectFactory 獲取依賴對(duì)象,最后通過反射調(diào)用 setter 方法將依賴設(shè)置到目標(biāo)對(duì)象中。整個(gè)過程對(duì)應(yīng)的代碼如下:
private T injectExtension(T instance) { try { if (objectFactory != null) { // 遍歷目標(biāo)類的所有方法 for (Method method : instance.getClass().getMethods()) { // 檢測(cè)方法是否以 set 開頭,且方法僅有一個(gè)參數(shù),且方法訪問級(jí)別為 public if (method.getName().startsWith("set") && method.getParameterTypes().length == 1 && Modifier.isPublic(method.getModifiers())) { // 獲取 setter 方法參數(shù)類型 Class<?> pt = method.getParameterTypes()[0]; try { // 獲取屬性名,比如 setName 方法對(duì)應(yīng)屬性名 name String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : ""; // 從 ObjectFactory 中獲取依賴對(duì)象 Object object = objectFactory.getExtension(pt, property); if (object != null) { // 通過反射調(diào)用 setter 方法設(shè)置依賴 method.invoke(instance, object); } } catch (Exception e) { logger.error("fail to inject via method..."); } } } } } catch (Exception e) { logger.error(e.getMessage(), e); } return instance; }
在上面代碼中,objectFactory 變量的類型為 AdaptiveExtensionFactory,AdaptiveExtensionFactory 內(nèi)部維護(hù)了一個(gè) ExtensionFactory 列表,用于存儲(chǔ)其他類型的 ExtensionFactory。Dubbo 目前提供了兩種 ExtensionFactory,分別是 SpiExtensionFactory 和 SpringExtensionFactory。前者用于創(chuàng)建自適應(yīng)的拓展,后者是用于從 Spring 的 IOC 容器中獲取所需的拓展。這兩個(gè)類的類的代碼不是很復(fù)雜,這里就不一一分析了。
Dubbo IOC 目前僅支持 setter 方式注入,總的來說,邏輯比較簡(jiǎn)單易懂。
動(dòng)態(tài)增強(qiáng)
在用Spring的時(shí)候,我們經(jīng)常會(huì)用到AOP功能。在目標(biāo)類的方法前后插入其他邏輯。比如通常使用Spring AOP來實(shí)現(xiàn)日志,監(jiān)控和鑒權(quán)等功能。 Dubbo的擴(kuò)展機(jī)制,是否也支持類似的功能呢?答案是yes。在Dubbo中,有一種特殊的類,被稱為Wrapper類。通過裝飾者模式,使用包裝類包裝原始的擴(kuò)展點(diǎn)實(shí)例。在原始擴(kuò)展點(diǎn)實(shí)現(xiàn)前后插入其他邏輯,實(shí)現(xiàn)AOP功能。
裝飾者模式
裝飾者模式:在不改變?cè)愇募约安皇褂美^承的情況下,動(dòng)態(tài)地將責(zé)任附加到對(duì)象上,從而實(shí)現(xiàn)動(dòng)態(tài)拓展一個(gè)對(duì)象的功能。它是通過創(chuàng)建一個(gè)包裝對(duì)象,也就是裝飾來包裹真實(shí)的對(duì)象。
一般來說裝飾者模式有下面幾個(gè)參與者:
- Component:裝飾者和被裝飾者共同的父類,是一個(gè)接口或者抽象類,用來定義基本行為
- ConcreteComponent:定義具體對(duì)象,即被裝飾者
- Decorator:抽象裝飾者,繼承自Component,從外類來擴(kuò)展ConcreteComponent。對(duì)于ConcreteComponent來說,不需要知道Decorator的存在,Decorator是一個(gè)接口或抽象類
- ConcreteDecorator:具體裝飾者,用于擴(kuò)展ConcreteComponent
注:裝飾者和被裝飾者對(duì)象有相同的超類型,因?yàn)檠b飾者和被裝飾者必須是一樣的類型,這里利用繼承是為了達(dá)到類型匹配,而不是利用繼承獲得行為。
dubbo中的AOP
Dubbo AOP 是通過裝飾者模式完成的,接下來通過一個(gè)簡(jiǎn)單的案例來學(xué)習(xí)dubbo中AOP的實(shí)現(xiàn)方式。
首先定義一個(gè)接口
package com.itheima.dubbo; import org.apache.dubbo.common.extension.SPI; @SPI public interface Phone { void call(); }
定義接口的實(shí)現(xiàn)類,也就是被裝飾者
package com.itheima.dubbo; public class IphoneX implements Phone { @Override public void call() { System.out.println("iphone正在撥打電話"); } }
為了簡(jiǎn)單,這里省略了裝飾者接口。僅僅定義一個(gè)裝飾者,實(shí)現(xiàn)phone接口,內(nèi)部配置增強(qiáng)邏輯方法
package com.itheima.dubbo; public class MusicPhone implements Phone { private Phone phone; public MusicPhone(Phone phone) { this.phone = phone; } @Override public void call() { System.out.println("播放彩鈴"); this.phone.call(); } }
```
添加拓展點(diǎn)配置文件META-INF/dubbo/com.itheima.dubbo.Phone,內(nèi)容如下
```
iphone = com.itheima.dubbo.IphoneX
filter = com.itheima.dubbo.MusicPhone
```
配置測(cè)試方法
public static void main(String[] args) { ExtensionLoader<Phone> extensionLoader = ExtensionLoader.getExtensionLoader(Phone.class); Phone phone = extensionLoader.getExtension("iphone"); phone.call(); }
具體執(zhí)行效果如下
先調(diào)用裝飾者增強(qiáng),再調(diào)用目標(biāo)方法完成業(yè)務(wù)邏輯。
通過測(cè)試案例,可以看到在Dubbo SPI中具有增強(qiáng)AOP的功能,我們只需要關(guān)注dubbo源碼中這樣一行代碼就夠了。
//檢查是否具有裝飾者類,如果有調(diào)用裝飾者類的構(gòu)造方法,并返回實(shí)例對(duì)象 if (CollectionUtils.isNotEmpty(wrapperClasses)) { for (Class<?> wrapperClass : wrapperClasses) { instance = injectExtension( (T) wrapperClass.getConstructor(type).newInstance(instance)); } }
動(dòng)態(tài)編譯
SPI中的自適應(yīng)
我們知道在 Dubbo 中,很多拓展都是通過 SPI 機(jī)制 進(jìn)行加載的,比如 Protocol、Cluster、LoadBalance、ProxyFactory 等。有時(shí),有些拓展并不想在框架啟動(dòng)階段被加載,而是希望在拓展方法被調(diào)用時(shí),根據(jù)運(yùn)行時(shí)參數(shù)進(jìn)行加載,即根據(jù)參數(shù)動(dòng)態(tài)加載實(shí)現(xiàn)類。如下所示:
這種在運(yùn)行時(shí),根據(jù)方法參數(shù)才動(dòng)態(tài)決定使用具體的拓展,在dubbo中就叫做擴(kuò)展點(diǎn)自適應(yīng)實(shí)例。其實(shí)是一個(gè)擴(kuò)展點(diǎn)的代理,將擴(kuò)展的選擇從Dubbo啟動(dòng)時(shí),延遲到RPC調(diào)用時(shí)。Dubbo中每一個(gè)擴(kuò)展點(diǎn)都有一個(gè)自適應(yīng)類,如果沒有顯式提供,Dubbo會(huì)自動(dòng)為我們創(chuàng)建一個(gè),默認(rèn)使用Javaassist。
自適應(yīng)拓展機(jī)制的實(shí)現(xiàn)邏輯是這樣的
1. 首先 Dubbo 會(huì)為拓展接口生成具有代理功能的代碼;
2. 通過 javassist 或 jdk 編譯這段代碼,得到 Class 類;
3. 通過反射創(chuàng)建代理類;
4. 在代理類中,通過URL對(duì)象的參數(shù)來確定到底調(diào)用哪個(gè)實(shí)現(xiàn)類;
javassist入門
Javassist是一個(gè)開源的分析、編輯和創(chuàng)建Java字節(jié)碼的類庫。是由東京工業(yè)大學(xué)的數(shù)學(xué)和計(jì)算機(jī)科學(xué)系的 Shigeru Chiba (千葉滋)所創(chuàng)建的。它已加入了開放源代碼JBoss 應(yīng)用服務(wù)器項(xiàng)目,通過使用Javassist對(duì)字節(jié)碼操作為JBoss實(shí)現(xiàn)動(dòng)態(tài)AOP框架。javassist是jboss的一個(gè)子項(xiàng)目,其主要的優(yōu)點(diǎn),在于簡(jiǎn)單,而且快速。直接使用java編碼的形式,而不需要了解虛擬機(jī)指令,就能動(dòng)態(tài)改變類的結(jié)構(gòu),或者動(dòng)態(tài)生成類。為了方便更好的理解dubbo中的自適應(yīng),這里通過案例的形式來熟悉下Javassist的基本使用。
package com.itheima.compiler; import java.io.File; import java.io.FileOutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; import javassist.CtField; import javassist.CtMethod; import javassist.CtNewMethod; /** * Javassist是一個(gè)開源的分析、編輯和創(chuàng)建Java字節(jié)碼的類庫 * 能動(dòng)態(tài)改變類的結(jié)構(gòu),或者動(dòng)態(tài)生成類 */ public class CompilerByJavassist { public static void main(String[] args) throws Exception { // ClassPool:class對(duì)象容器 ClassPool pool = ClassPool.getDefault(); // 通過ClassPool生成一個(gè)User類 CtClass ctClass = pool.makeClass("com.itheima.domain.User"); // 添加屬性 -- private String username CtField enameField = new CtField(pool.getCtClass("java.lang.String"), "username", ctClass); enameField.setModifiers(Modifier.PRIVATE); ctClass.addField(enameField); // 添加屬性 -- private int age CtField enoField = new CtField(pool.getCtClass("int"), "age", ctClass); enoField.setModifiers(Modifier.PRIVATE); ctClass.addField(enoField); //添加方法 ctClass.addMethod(CtNewMethod.getter("getUsername", enameField)); ctClass.addMethod(CtNewMethod.setter("setUsername", enameField)); ctClass.addMethod(CtNewMethod.getter("getAge", enoField)); ctClass.addMethod(CtNewMethod.setter("setAge", enoField)); // 無參構(gòu)造器 CtConstructor constructor = new CtConstructor(null, ctClass); constructor.setBody("{}"); ctClass.addConstructor(constructor); // 添加構(gòu)造函數(shù) //ctClass.addConstructor(new CtConstructor(new CtClass[] {}, ctClass)); CtConstructor ctConstructor = new CtConstructor(new CtClass[] {pool.get(String.class.getName()),CtClass.intType}, ctClass); ctConstructor.setBody("{\n this.username=$1; \n this.age=$2;\n}"); ctClass.addConstructor(ctConstructor); // 添加自定義方法 CtMethod ctMethod = new CtMethod(CtClass.voidType, "printUser",new CtClass[] {}, ctClass); // 為自定義方法設(shè)置修飾符 ctMethod.setModifiers(Modifier.PUBLIC); // 為自定義方法設(shè)置函數(shù)體 StringBuffer buffer2 = new StringBuffer(); buffer2.append("{\nSystem.out.println(\"用戶信息如下\");\n") .append("System.out.println(\"用戶名=\"+username);\n") .append("System.out.println(\"年齡=\"+age);\n").append("}"); ctMethod.setBody(buffer2.toString()); ctClass.addMethod(ctMethod); //生成一個(gè)class Class<?> clazz = ctClass.toClass(); Constructor cons2 = clazz.getDeclaredConstructor(String.class,Integer.TYPE); Object obj = cons2.newInstance("itheima",20); //反射 執(zhí)行方法 obj.getClass().getMethod("printUser", new Class[] {}) .invoke(obj, new Object[] {}); // 把生成的class文件寫入文件 byte[] byteArr = ctClass.toBytecode(); FileOutputStream fos = new FileOutputStream(new File("D://User.class")); fos.write(byteArr); fos.close(); } }
通過以上代碼,我們可以知道使用javassist可以方便的在運(yùn)行時(shí),按需動(dòng)態(tài)的創(chuàng)建java對(duì)象,并執(zhí)行內(nèi)部方法。而這也是dubbo中動(dòng)態(tài)編譯的核心。
源碼分析
Adaptive注解
在開始之前,我們有必要先看一下與自適應(yīng)拓展息息相關(guān)的一個(gè)注解,即 Adaptive 注解。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface Adaptive { String[] value() default {}; }
從上面的代碼中可知,Adaptive 可注解在類或方法上。
- 標(biāo)注在類上:Dubbo 不會(huì)為該類生成代理類。
- 標(biāo)注在方法上:Dubbo 則會(huì)為該方法生成代理邏輯,表示當(dāng)前方法需要根據(jù) 參數(shù)URL 調(diào)用對(duì)應(yīng)的擴(kuò)展點(diǎn)實(shí)現(xiàn)。
獲取自適應(yīng)拓展類
dubbo中每一個(gè)擴(kuò)展點(diǎn)都有一個(gè)自適應(yīng)類,如果沒有顯式提供,Dubbo會(huì)自動(dòng)為我們創(chuàng)建一個(gè),默認(rèn)使用Javaassist。 先來看下創(chuàng)建自適應(yīng)擴(kuò)展類的代碼:
public T getAdaptiveExtension() { Object instance = cachedAdaptiveInstance.get(); if (instance == null) { synchronized (cachedAdaptiveInstance) { instance = cachedAdaptiveInstance.get(); if (instance == null) { instance = createAdaptiveExtension(); cachedAdaptiveInstance.set(instance); } } } return (T) instance; }
繼續(xù)看createAdaptiveExtension方法
```java private T createAdaptiveExtension() { return injectExtension((T) getAdaptiveExtensionClass().newInstance()); } ```
繼續(xù)看getAdaptiveExtensionClass方法
private Class<?> getAdaptiveExtensionClass() { getExtensionClasses(); if (cachedAdaptiveClass != null) { return cachedAdaptiveClass; } return cachedAdaptiveClass = createAdaptiveExtensionClass(); }
繼續(xù)看createAdaptiveExtensionClass方法,繞了一大圈,終于來到了具體的實(shí)現(xiàn)了。看這個(gè)createAdaptiveExtensionClass方法,它首先會(huì)生成自適應(yīng)類的Java源碼,然后再將源碼編譯成Java的字節(jié)碼,加載到JVM中。
private Class<?> createAdaptiveExtensionClass() { String code = createAdaptiveExtensionClassCode(); ClassLoader classLoader = findClassLoader(); org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); return compiler.compile(code, classLoader); } ```
Compiler的代碼,默認(rèn)實(shí)現(xiàn)是javassist。
@SPI("javassist") public interface Compiler { Class<?> compile(String code, ClassLoader classLoader); }
createAdaptiveExtensionClassCode()方法中使用一個(gè)StringBuilder來構(gòu)建自適應(yīng)類的Java源碼。方法實(shí)現(xiàn)比較長(zhǎng),這里就不貼代碼了。這種生成字節(jié)碼的方式也挺有意思的,先生成Java源代碼,然后編譯,加載到j(luò)vm中。通過這種方式,可以更好的控制生成的Java類。而且這樣也不用care各個(gè)字節(jié)碼生成框架的api等。因?yàn)閤xx.java文件是Java通用的,也是我們最熟悉的。只是代碼的可讀性不強(qiáng),需要一點(diǎn)一點(diǎn)構(gòu)建xx.java的內(nèi)容。