快來!我從原始碼中學習到了一招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,一個被程式碼耽誤的文學創作者,不是大佬,但是喜歡分享,是一個又暖又有料的四川好男人。