快來!我從原始碼中學習到了一招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:
EchoService demoService = (EchoService) this.demoService;
String echo = (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,一個被程式碼耽誤的文學創作者,不是大佬,但是喜歡分享,是一個又暖又有料的四川好男人。
相關推薦
快來!我從原始碼中學習到了一招Dubbo的騷操作!
荒腔走板 大家好,我是 why,歡迎來到我連續周更優質原創文章的第 55 篇。 老規矩,先來一個簡短的荒腔走板,給冰冷的技術文注入一絲色彩。 魔幻的 2020 年的上半年過去了,很多人都在朋友圈和上半年說再見,我也不例外。 上面這張照片,就是我在朋友圈發的一張圖片。 這張照片是我在公司去年年會的時候拍
從原始碼中學習設計模式系列——單例模式序/反序列化以及反射攻擊的問題(二)
一、前言 這篇文章是學習單例模式的第二篇,之前的文章一下子就給出來看起來很高大上的實現方法,但是這種模式還是存在漏洞的,具體有什麼問題,大家可以停頓一會兒,思考一下。好了,不賣關子了,下面我們來看看每種單例模式存在的問題以及解決辦法。 二、每種Singleton 模式的演進 模式一
高明!OpenAI提出HER演算法,AI系統學會從錯誤中學習
失敗是成功之母:HER有自我審視能力最近幾個月,OpenAI的研究人員集中精力於構建具有更強的學習能力的人工智慧。得益於他們的增強學習系統OpenAI baselines,機器學習演算法可以進行自主學習。目前,這個新的演算法保證人工智慧可以像人類一樣從自己的錯誤中汲取教訓。這
【肥宅捕獲指南】快來給我寫小紙條吧!
### 前言 前段時間把之前買的咕咕機從老姐那裡要了回來(霧),準備對這個單機版印表機做一個擴充套件,根據官方給出的介面來把它擴充套件成一個可以讓諸多網友給我傳送小紙條的聯機印表機。 用了一早上把介面實
Python,我從零開始學習T^T D7
int res key def yar bsp cti for 多個 當函數遇到不確定數量參數腫麽破?*args和**kwargs前來報道! *args **kwargs 當函數的參數不確定時,可以使用*args 和**kwargs,*args
4星|《為何要提升員工的幸福感》:幸福感最高的三大要素是:感恩的習慣、開放的心態,從錯誤中學習的能力,以及擁有自己的人生意義
計算機 開心 image 保持 這就是我 創造力 log 高效 電子書 《哈佛商業評論》的4篇文章。不確定是不是以前的舊文集結出版還是新寫/譯的。 有一些針對幸福感的調研數據,篇幅雖小但是這樣的數據還是比較有意義的。書中一個重要的信息是:幸福感最高的三大要素是:感恩的
51CTO博客2.0新功能上線,快來跟我一起用一句話介紹自己
博客 proc HR BE cdc TP image pro 功能 在今天這個萬眾矚目世界杯,人人等待端午節的日子裏,我們51CTO博客2.0終於又迎來了一次版本更新。 現在大家跟我一起使用本次更新的新功能,用一句話介紹自己吧!! Q:一句話簡介可以在那兒被看到?
從專案中學習HTML+CSS
title: 從專案中學習HTML+CSS tags: [HTML, CSS, Web開發] date: 2018-11-10 10:51:51 categories: Web開發 keywords: HTML, CSS, Web開發 最近由於工作原因以及自己的懈怠,已經很久都
跟我從零基礎學習Unity3D開發--資源打包篇(AssetBundle)
好久沒更新了,一直在加班敢專案進度。這裡和關注我的部落格的童鞋表示一下歉意!這裡有我錄的Unity3D從零開始的視訊教程大家可以關注一下:http://www.imooc.com/view/555 視訊會陸續的更新,需要講什麼也可以留言給我。 之前在專案中一直用的是別人以前的寫的打包
從程式中學習UKF-SLAM(一)
然間從GitHub上發現一個小專案,做的是一個基於UKF的狀態估計,使用C++版本的mathplot做顯示,編譯之後卻發現顯示一直有問題,於是萌生了一個想法:能不能把它移植到ROS中?然後就有了接下來的學習過程,以此記錄。如有問題,歡迎提出。 這是需要複寫的程式
“我們不是夫妻,快來救救我”
男子火車上突然拽住列車長: “我們不是夫妻,快救救我” 小兩口“吵架”男的喊救命 據瞭解,3月8日晚上11時許,K1138次南寧開往青島的列車在永州車站開車後,車上不少旅客已經休息。列車長孫權和乘警王亞林像往常一樣,一起巡視車廂,兩人走到9號臥鋪車廂的連線處時,看到一對像是夫妻
【Yaser S. Abu-Mostafa課件】從資料中學習——三個學習原則
本課件主要內容包括: 重複的主題:簡單假設 “奧卡姆剃刀”原則 第一個問題:“簡單”意味著什麼? 第二個問題:為什麼越簡單越好? 偏差 分佈函式匹配 重用資料集 兩種補救方法 完整課件
【Yaser S. Abu-Mostafa課件】從資料中學習——徑向基函式
本課件主要內容包括: 基本RBF模型 學習演算法 用於分類的RBF 與最近鄰方法的關係 K箇中心的RBF 中心選擇的問題 迭代演算法 Lloyd演算法 中心與支援向量 RBF網路
【Yaser S. Abu-Mostafa課件】從資料中學習——核方法
本課件主要包括: 廣義內積 處理技巧 多項式核 SVM的核公式 設計自己的核函式 兩種不可分型別的例子 誤差測量 拉格朗日乘子 支援向量的型別 完整課件下載地址: htt
IOLI crackme分析——從應用中學習使用radare2
Crackme0x00 - writeup 我現在開始看radare2book了,現在剛看1/3,有些無聊,因為之前也看過一些radare2的例項講解,所以現在先試著做一下里面的crackme練習。 先執行一下craceme0x00這個檔案,看來是要把密碼找出來。 [email prot
Android 網路框架之Retrofit2使用詳解及從原始碼中解析原理
就目前來說Retrofit2使用的已相當的廣泛,那麼我們先來了解下兩個問題: 1 . 什麼是Retrofit? Retrofit是針對於Android/Java的、基於okHttp的、一種輕量級且安全的、並使用註解方式的網路請求框架。 2 . 我們為什麼要
從原始碼中檢視當前android版本
1: 系統沒有編譯,從程式碼中檢視當前版本 vim build/core/version_defaults.mk # Default versions for each TARGET_PLATFORM_VERSION # TODO: PLATFORM_VERSION,
asp.net mvc 之旅 —— 第五站 從原始碼中分析asp.net mvc 中的TempData
在mvc的controller中,我們知道有很多的臨時變數存放資料,比如說viewData,viewBag,還有一個比較特殊的tempData,關於前兩個或許大家都明白, 基本上是一個東西,就是各自的程式設計寫法不一樣,最終都會放到viewContext中,然後送到WebPa
android out目錄從原始碼中分離出來
Android系統編譯成功後的程式碼預設放在原始碼目錄下的out目錄中,我們需要將out目錄分離出來,可以在環境變數中新增 OUT_DIR_COMMON_BASE="/androidsource/out",如果你的工作目錄中存在 /work/android_branch1,
Unity從實踐中學習(1)
info 博客 tail 離開 想法 探索 alt 跟蹤 地圖 首先在實際中unity的開發之中快捷鍵應該是相當重要的一個部分,這裏先引用csdn的一個博客,https://blog.csdn.net/qq_34552886/article/details/69775013