1. 程式人生 > 實用技巧 >快來!我從原始碼中學習到了一招Dubbo的騷操作!

快來!我從原始碼中學習到了一招Dubbo的騷操作!

荒腔走板

大家好,我是 why,歡迎來到我連續周更優質原創文章的第 55 篇。

老規矩,先來一個簡短的荒腔走板,給冰冷的技術文注入一絲色彩。

魔幻的 2020 年的上半年過去了,很多人都在朋友圈和上半年說再見,我也不例外。

上面這張照片,就是我在朋友圈發的一張圖片。

這張照片是我在公司去年年會的時候拍的,出處來自電影《飛馳人生》。

電影裡面有人問張弛:你五年連續獲得冠軍的必勝絕招是什麼?

張馳滿懷深情的回答:必勝絕招只有兩個字—奉獻。就是把你的全部,奉獻給你所熱愛的一切。

什麼是熱愛?

可以用電影裡面的一句臺詞來回答:

“巴音布魯克,1462道彎,109公里,耍小聰明,贏得了100米,贏不了100公里。我每天在腦海裡開20遍,5年,3萬6千遍,我能記住每一個彎道。”

張弛在電影裡面是一個卑微的角色,他賣炒飯、賣唱、偷車架、端著飯碗喝紅酒......做了很多很多卑微的事情。

但是,他的心裡一直記得巴音布魯克,一直記得那 1462 個彎道。即使卑微到塵土,他最終還是拼了命的回到了賽道。

這就是熱愛。

熱愛,從來不是一件簡單的事情。

這句話也讓我想起了路遙先生在《早晨從中午開始》中的一句話:

只有初戀般的熱情和宗教般的意志,人才有可能成就某種事業。

這也是熱愛,對畢生所最追求之熱愛。

我是一個普普通通的程式猿,但是我喜歡這個行業;我是一個平凡無奇的打工仔,但是我熱愛我的生活。

你呢?你熱愛著什麼?又付出了多少?

2020 年的上半年,我每一天都在努力。

2020 年的下半年,願你我共同成長。

好了,說迴文章。

先說背景

前段時間有個讀者問我,他說他們的 RPC 框架用的是 Dubbo,當對接一個新服務的介面時就需要開通對應的網路關係。

比如我是 A 服務,第一次對接 B 服務的 Dubbo 介面,那麼我需要開通 A 服務到 B 服務的對應的 Dubbo 埠的網路訪問許可權。

但是有的時候總是有人忘記開通網路許可權,導致業務展開的時候服務呼叫報錯。已經吃過幾次這樣的虧了。

目前他們想到的解決方案是 A 服務啟動後就呼叫 B 服務提供的一個專門用於測試能否調通的介面。如果不通,配合監控手段,這樣就能主動發現問題了。

這是一個兜底方案,防止開發人員忘記或者不知道需要開通網路許可權的情況。

這個解決方案的問題是每個服務都需要專門寫一個介面,以供其他服務來呼叫。

每一個服務都要寫,對系統的侵入性太大了。

有沒有什麼好的解決方案呢?

大家想想呢,這種問題其實還是挺普遍的。有點類似於心跳功能,雖然只需要跳一次。

Dubbo 服務啟動成功後,你怎麼主動判斷需要用到的介面,都是可以訪問到的?

瞭解到這問題後,我就回復了兩段內容。

第一段是:Dubbo 啟動時檢查瞭解一下?回聲測試瞭解一下?

第二段是:這樣做除了每個服務都需要專門寫一個介面外,還需要考慮一個情況。B 服務叢集部署,比如有三個節點,負載均衡之後只會選擇一個其中一個。如果恰好這個服務是開通了網路關係,但是另外兩個都忘記了呢?怎麼做?

文字就主要圍繞這兩個問題展開,重點是對回聲測試的實現原理的剖析,看完之後你會由衷的感嘆一句:這程式碼,使用了障眼法呀,是真的“騷”啊。

需要說明一下的是,本文中涉及到的原始碼均為目前最新的 Dubbo 2.7.7 版本。

啟動時檢查

在說回聲測試之前,我得先簡單的提一下 Dubbo 的啟動時檢查。

上面提到的這個問題,Dubbo 肯定也是考慮到了的,啟動的時候就應該去檢查依賴的服務是否可用。

我們看一下官網上怎麼說的:

http://dubbo.apache.org/zh-cn/docs/user/demos/preflight-check.html

意思就是這個 check 你可以用但是有的場景下它支援的不是太好。

我一般是不用,會設定為 false。

那麼這個引數怎麼配置,可以在哪配置呢?

還是去看官網啊,寫的很清楚的:

這是一種解決方案,但不是本文重點,所以這一節只是做介紹,實現原理不進行展開,有興趣的朋友可以自己去翻翻原始碼。

啥是回聲測試?

就算你們的 PRC 框架用的是 Dubbo,可能你根本就不知道回聲測試這回事。

很正常,關於這部分的介紹官網上都寫的極簡,所有加一塊,就只有這些內容:

http://dubbo.apache.org/zh-cn/docs/user/demos/echo-service.html

雖然你沒有關心過回聲測試,但是你的每一個 Dubbo 介面都支援回聲測試。

這點我們從官網上的描述也可以看出來的:

所有服務自動實現 EchoService 介面,只需將任意服務引用強制轉型為 EchoService,即可使用。

潤物無聲,牛不牛皮,驚不驚訝?

先整一個簡單、直觀的示例。

下面是一個 Dubbo 的介面(provider 端)和其實現類:

在 consumer 端進行呼叫,並輸出呼叫結果如下:

第 26 行呼叫 sayHello 方法沒啥說的,常規操作。

妙就妙在 28 和 29 行。

把 demoService 強轉成了 EchoService,然後這個方法還有一個 $echo 方法。

這個方法的入參和出參都是 Object 型別:

在上面的案例中,輸入“echo,why技術”,返回也是“echo,why技術”。

所以,EchoService 介面的 $echo 官方叫法是:回聲測試。

很形象,是不是?

用法是非常簡單了。總體來看就是如果你只需要看看 Dubbo 服務能否調通,但你又不想用啟動時檢查的方式,你也不需要為每個服務都專門提供一個諸如 sayHello 這樣的介面。

呼叫方只需要把其中的一個服務引用強轉為 EchoService 就可以了。

EchoService 就是一個介面:

框架已經給我們提供了這樣的功能,接下來,帶大家看看它的實現原理。 EchoService實現原理-大膽假設

用法是很簡單的,就是把 demoService 這個服務引用強轉為 EchoService:

EchoServicedemoService=(EchoService)this.demoService;
Stringecho=(String)demoService.$echo("echo,why技術");

只看上面這兩行程式碼,其實大家應該就可以猜出一個大概。

首先第一行是一個型別強轉,那麼說明 demoService 這個代理類,不僅實現了 DemoService 介面,還在某個不為人知的地方實現了 EchoService 這個介面。

就類似於這樣式兒的:

public class 代理類 implements DemoService, EchoService

因為只有這樣強轉的時候才不會報錯。

然後第二行呼叫了 $echo 方法,一定是某個地方實現了這個介面,實現方式裡面保持出參和入參一致。

所以我們提出兩點猜測:

1.DemoService 這個服務引用是由框架幫我們實現了 EchoService 介面。

2.同時框架幫我們實現了 $echo 方法,方法的邏輯是保證其出參和入參一致。

接著我們就去驗證一下。

EchoService實現原理-小心求證

先看截圖:

demoService 這個服務引用是一個動態代理的類。

可以清楚的看到,它其實是有三個方法的:

EchoService 的 $echo 方法。這個方法就是我們要找的方法。

DemoService 的 sayHello 方法。這個方法是我們提供的方法。

Destroyable 的 $destory 方法。這個方法可以先不關心,最後我會簡單的說一下。

所以,接下來,我們只需要找到生成動態代理類的地方,把 Dubbo 給我們生成的動態代理類打印出來,看一下就知道了是怎麼回事了。

那麼,我們在哪裡建立的代理物件呢?

程式碼的入口為:

org.apache.dubbo.rpc.ProxyFactory#getProxy(org.apache.dubbo.rpc.Invoker<T>)

可以看到,這是一個 SPI 介面:

其預設實現是 javassist 的方式。

這個 SPI 介面的實現類有下面這三個:

stub 是做本地存根用的,不是本文重點,大家瞭解一下就行,其對應的官網介紹如下:

http://dubbo.apache.org/zh-cn/docs/user/demos/local-stub.html

jdk 和 javassist 是代理工廠的具體實現。

那為什麼沒有用 CGLIB 呢?

別問,問就是:別慌,等下再說。

到這裡,面試題也就隨之而來了:

請問 Dubbo 提供了哪些動態代理的實現方式?其預設實現是什麼呢?

記住啦,只有 jdk 和 javassist 的實現方法,沒有 CGLIB。其預設實現是 javassist。

所以,接下來我們主要看看 javassist 的實現過程:

在下面方法的第 79 行打上斷點:

org.apache.dubbo.rpc.proxy.AbstractProxyFactory#getProxy(org.apache.dubbo.rpc.Invoker<T>, boolean)

標號為 ① 的地方是獲取 interfaces 配置,本文中示例為 null,所以不會走進該 if 分支中。

標號為 ② 的地方是判斷是否需要泛化呼叫,預設是 false。

標號為 ③ 的地方才是我們需要關注的地方。

喲,這不是巧了嗎,這不是?

這裡有我們自己的介面 DemoService,還有我們要找的介面 EchoService。

接下來 79 行會去呼叫 82 行的抽象方法 getProxy:

而這個方法,前面我們說了,有兩個實現類。我們主要看預設實現 javassist。

最終會走到這個方法來:

org.apache.dubbo.common.bytecode.Proxy#getProxy(java.lang.ClassLoader, java.lang.Class<?>...)

這個方法的程式碼特別長,而且很難讀懂。所以我就不帶著大家一行行的解讀了。

先看個大概:

主要是要理解 136 行的 ccp 和 ccm 是幹啥的。這是這個方法最重要的東西。

ccp 用於為服務介面生成代理類,我們示例中的 DemoService 介面的動態代理物件,就是由 ccp 生成的。

ccm 用於為 org.apache.dubbo.common.bytecode.Proxy 抽象類生成子類,主要是實現 Proxy 類的 newInstance 抽象方法。

我常常說原始碼之下無祕密,這兩個類是由原始碼生成的原始碼,不能直觀的看到。

接下來,配合 idea 的 Evaluate Expression 計算表示式視窗教大家一個騷操作。

在 Debug 模式下,按快捷鍵 Alt + F8 就可以開啟Evaluate Expression計算表示式視窗。

先看 ccp,通過 debugWriterFile 命令就能把生成的代理類寫到本地(注意是首字母小寫的 proxy0):

同理,ccm 也可以這樣取出來,這裡我們換一個目錄(注意是首字母大寫的 Proxy0)::

然後我們把生成在本地的代理類開啟看一下,D 盤這個 Proxy0.class 就是 ccm 生成的,很簡單,大家看一下就行:

玄機就藏在 13 行這個 proxy0 裡面,而這個 proxy0,就是 ccp 生成的動態代理物件,也就是我們放在 E 盤的 proxy0:

從 15 行可以看出,這個代理類不僅實現了我們的 DemoService 介面,還悄悄幫我們實現了 EchoService 介面。

所以我們之前的第一個猜測是正確的。DemoService 這個服務引用是由框架幫我們實現了 EchoService 介面。

這樣,強制型別轉換的時候就不會有問題了。

那麼這個介面的方法 $echo 是怎麼實現的呢?

你只有一個動態代理也沒有用啊,沒有地方去實現這個方法,真正呼叫的時候也會出錯的呀。

這個時候就要祭出 Dubbo 的 Filter 鏈了:

在 EchoFilter 這個攔截器裡面,判斷瞭如果呼叫方法是 $echo,有且僅有一個引數,就直接把引數返回。

走到這個 EchoFilter 攔截器了,就說明服務是可用的了,探測任務已經完成,也就不需要繼續往下走了。

在這個過程中,這個 EchoFilter 攔截器相當於是方法的具體實現了。

動態代理的類裡面有這個方法,但實際上這個方法沒有具體實現。

這是障眼法啊,這操作夠騷啊。

所以,我們前面的這個猜測是不正確的:框架幫我們實現了 $echo 方法,方法的邏輯是保證其出參和入參一致。

框架並沒有幫我們實現 $echo 方法,而是基於其攔截鏈機制,攔截到是這個方法後,就返回入參,相當於另外一種方法的實現。

有的同學就說了,我的系統裡面倒是用到了動態代理,但是我也沒有這種攔截鏈的機制啊。

朋友,思維發散點,別隻盯著攔截鏈呀。

給大家簡單的看一下 $destory 方法的操作方式,你就明白了。

這是 Dubbo 2.7.5 版本之後加入的停機相關的方法,也是所有代理物件都自動實現 Destroyable 介面。

給大家上一個對比圖吧,左邊是 Dubbo 2.7.4.1 版本生成的動態代理類,右邊是Dubbo 2.7.7 版本生成的動態代理類:

$destory 這個方法的障眼法是怎麼使的呢?還是基於 Filter 嗎?

你想一想,現在是要銷燬這個代理了,是不是應該在方法呼叫的時候就立即觸發了,還花這麼大勁走到 Filter 裡面去幹啥?

給大家演示一下:

這個方法是怎麼被攔截的呢?

請看,直接在 invoke 裡面,方法呼叫的入口處就“硬編碼”的攔截住了,就是這麼靈性:

和destory和echo 的實現差不多,只是攔截時機不同而已。

所以,其實這就是一種思想,基於動態代理我們可以搞很多事情,介面裡面的方法,也不是非得實現,只要我們能攔截到這個方法就行。

關鍵是,你得分析清楚,在什麼時機去攔截。

所以,我們能從 Dubbo 原始碼中學到的這個騷操作是在建立動態代理物件的時候,可以神不知鬼不覺的給代理物件加一個介面,而且不需要真正的去實現介面裡面的方法,只需要攔截下來就行。

這個時候,你再回想回想 Mybatis ,是不是也是隻有介面,沒有實現類,也是通過動態代理的方式把介面和 SQL 關聯起來的。

你就想,多聯想,品一品這個味道。自己多咂摸咂摸。

Filter裡面搞點事情

$echo 既然它是基於 EchoFilter 的,而 Filter 又是一個 SPI 介面。那我們又可以搞事情了。

比如我們小小的改動一下,返回這個請求是負載到了哪個服務提供者中:

需要注意的是我們的自定義 Filter 需要在框架的 EchoFilter 之前執行。

所以,我們的 order 需要比 EchoFilter 小一點。

至於怎麼配置讓我們自定義的 WhyEchoFilter 生效,這裡就不介紹了,大家可以去查一下。

配置好之後,跑一下測試用例,就會走到我們自定義的 WhyEchoFilter 中:

可以看到,輸出的時候帶出了這個請求是負載到了 20882 埠的服務提供者。

這裡只是一個小例子,invoker 引數裡面的資訊非常的豐富,大家可以自由發揮。 叢集模式怎麼搞

不知道大家有沒有發現一個問題。

一次請求只會呼叫到一個服務提供者(負載均衡配置的是廣播模式的不在這次的考慮範圍內)。

一般來說我們都有兩個以上的服務提供者。

基本本文的需求,我們一次探測,應該呼叫到所有的服務提供者,這樣才放心。

所以,核心問題是要獲取到所有的服務提供者,那我們怎麼實現這個需求呢?

首先肯定不能在 Filter 裡面搞事情了,因為走到 Filter 的時候,已經經過負載均衡後選定了某一個服務提供者了。

我這裡沒有去實現這個需求,但是提供兩個思路,原始碼裡面都有,我們可以照葫蘆畫瓢:

第一個思路是看看 Dubbo 原始碼裡面怎麼獲取到所有 invokers 的:

org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker#invoke

第二個思路是看看 Dubbo-Admin 管理臺對應的原始碼裡面是怎麼獲取到這個列表的:

實現起來可能會有點麻煩,但是原始碼都擺在上面的兩個思路里面了。借鑑一下就行了。

最後說一句(求關注)

才疏學淺,難免會有紕漏,如果你發現了錯誤的地方,還請你留言指出來,我對其加以修改。

感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。

我是 why,一個被程式碼耽誤的文學創作者,不是大佬,但是喜歡分享,是一個又暖又有料的四川好男人。