https://mp.weixin.qq.com/s/qKZzTDwsG8-VJyE0EZvmvwhtml
你們好,我是 why,歡迎來到我連續周更優質原創文章的第 55 篇。
老規矩,先來一個簡短的荒腔走板,給冰冷的技術文注入一絲色彩。
魔幻的 2020 年的上半年過去了,不少人都在朋友圈和上半年說再見,我也不例外。
上面這張照片,就是我在朋友圈發的一張圖片。java
這張照片是我在公司去年年會的時候拍的,出處來自電影《飛馳人生》。
電影裏面有人問張弛:你五年連續得到冠軍的必勝絕招是什麼?
張馳滿懷深情的回答:必勝絕招只有兩個字—奉獻。就是把你的所有,奉獻給你所熱愛的一切。
什麼是熱愛?
能夠用電影裏面的一句臺詞來回答:
「巴音布魯克,1462道彎,109千米,耍小聰明,贏得了100米,贏不了100千米。我天天在腦海裏開20遍,5年,3萬6千遍,我能記住每個彎道。」
張弛在電影裏面是一個卑微的角色,他賣炒飯、賣唱、偷車架、端着飯碗喝紅酒......作了不少不少卑微的事情。
可是,他的內心一直記得巴音布魯克,一直記得那 1462 個彎道。即便卑微到塵土,他最終仍是拼了命的回到了賽道。
這就是熱愛。
熱愛,歷來不是一件簡單的事情。
這句話也讓我想起了路遙先生在《早晨從中午開始》中的一句話:
只有初戀般的熱情和宗教般的意志,人才有可能成就某種事業。
這也是熱愛,對畢生所最追求之熱愛。
我是一個普普統統的程序猿,可是我喜歡這個行業;我是一個平凡無奇的打工仔,可是我熱愛個人生活。
你呢?你熱愛着什麼?又付出了多少?
2020 年的上半年,我每一天都在努力。
2020 年的下半年,願你我共同成長。
好了,說迴文章。web
前段時間有個讀者問我,他說他們的 RPC 框架用的是 Dubbo,當對接一個新服務的接口時就須要開通對應的網絡關係。
好比我是 A 服務,第一次對接 B 服務的 Dubbo 接口,那麼我須要開通 A 服務到 B 服務的對應的 Dubbo 端口的網絡訪問權限。
可是有的時候老是有人忘記開通網絡權限,致使業務展開的時候服務調用報錯。已經吃過幾回這樣的虧了。
目前他們想到的解決方案是 A 服務啓動後就調用 B 服務提供的一個專門用於測試可否調通的接口。若是不通,配合監控手段,這樣就能主動發現問題了。
這是一個兜底方案,防止開發人員忘記或者不知道須要開通網絡權限的狀況。
這個解決方案的問題是每一個服務都須要專門寫一個接口,以供其餘服務來調用。
每個服務都要寫,對系統的侵入性太大了。
有沒有什麼好的解決方案呢?
你們想一想呢,這種問題其實仍是挺廣泛的。有點相似於心跳功能,雖然只須要跳一次。
Dubbo 服務啓動成功後,你怎麼主動判斷須要用到的接口,都是能夠訪問到的?
瞭解到這問題後,我就回復了兩段內容。
第一段是:Dubbo 啓動時檢查瞭解一下?回聲測試瞭解一下?
第二段是:這樣作除了每一個服務都須要專門寫一個接口外,還須要考慮一個狀況。B 服務集羣部署,好比有三個節點,負載均衡以後只會選擇一個其中一個。若是剛好這個服務是開通了網絡關係,可是另外兩個都忘記了呢?怎麼作?
文本就主要圍繞這兩個問題展開,重點是對回聲測試的實現原理的剖析,看完以後你會由衷的感嘆一句:這代碼,使用了障眼法呀,是真的「騷」啊。須要說明一下的是,本文中涉及到的源碼均爲目前最新的Dubbo 2.7.7 版本。面試
在說回聲測試以前,我得先簡單的提一下 Dubbo 的啓動時檢查。
上面提到的這個問題,Dubbo 確定也是考慮到了的,啓動的時候就應該去檢查依賴的服務是否可用。
咱們看一下官網上怎麼說的:
apache
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,便可使用。app
潤物無聲,牛不牛皮,驚不驚訝?負載均衡
先整一個簡單、直觀的示例。
下面是一個 Dubbo 的接口(provider 端)和其實現類:
框架
在 consumer 端進行調用,並輸出調用結果以下:
第 26 行調用 sayHello 方法沒啥說的,常規操做。
妙就妙在 28 和 29 行。
把 demoService 強轉成了 EchoService,而後這個方法還有一個 $echo 方法。
這個方法的入參和出參都是 Object 類型:
在上面的案例中,輸入「echo,why技術」,返回也是「echo,why技術」。
因此,EchoService 接口的 $echo 官方叫法是:回聲測試。
很形象,是否是?
用法是很是簡單了。整體來看就是若是你只須要看看 Dubbo 服務可否調通,但你又不想用啓動時檢查的方式,你也不須要爲每一個服務都專門提供一個諸如 sayHello 這樣的接口。
調用方只須要把其中的一個服務引用強轉爲 EchoService 就能夠了。
EchoService 就是一個接口:
框架已經給咱們提供了這樣的功能,接下來,帶你們看看它的實現原理。
用法是很簡單的,就是把 demoService 這個服務引用強轉爲 EchoService:
EchoService demoService = (EchoService).demoService;String echo = (String) demoService.$echo("echo,why技術");
只看上面這兩行代碼,其實你們應該就能夠猜出一個大概。
首先第一行是一個類型強轉,那麼說明 demoService 這個代理類,不只實現了 DemoService 接口,還在某個鮮爲人知的地方實現了 EchoService 這個接口。
就相似於這樣式兒的:
public class 代理類 implements DemoService, EchoService
由於只有這樣強轉的時候纔不會報錯。
而後第二行調用了 $echo 方法,必定是某個地方實現了這個接口,實現方式裏面保持出參和入參一致。
因此咱們提出兩點猜想:
DemoService 這個服務引用是由框架幫咱們實現了 EchoService 接口。
同時框架幫咱們實現了 $echo 方法,方法的邏輯是保證其出參和入參一致。
接着咱們就去驗證一下。
先看截圖:
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 行打上斷點:
標號爲 ① 的地方是獲取 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 關聯起來的。
你就想,多聯想,品一品這個味道。本身多咂摸咂摸。
$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 管理臺對應的源碼裏面是怎麼獲取到這個列表的:
實現起來可能會有點麻煩,可是源碼都擺在上面的兩個思路里面了。借鑑一下就好了。
前面還留下了一個問題:Dubbo 動態代理的實現爲何不用 CGLIB 的方式?
答案能夠在我最近看的《深刻理解JVM字節碼》這本書裏面找到。
這書的第 9 章《字節碼的應用》這一節裏面專門說到了字節碼在 Dubbo 上的應用:
這一節也提到了文章中說到的回聲測試:
在介紹字節碼在 Dubbo 上的應用時,他是這樣的說的:
Dubbo 的做者提到,使用 Javassit 來做爲動態代理方案的主要考慮因素是性能。在他們的性能測試中,性能 Javassit>cglib>JDK。
這個地方提到了 Dubbo 的做者的說法,可是沒有給出對應的依據。
因而,我去找了一下。
Dubbo 的做者是梁飛,而梁飛的幾篇博客就放在官網上的:
隨便打開一個,就能找到他的博客地址:
在他的博客裏面很容易就找到了對應的文章:
https://www.iteye.com/blog/javatar-814426
這篇文章他分別從下面幾個方面構成:
測試結果
測試結論
差別緣由
最終選型
測試代碼
字節碼對比
能夠說是有理有據,感興趣的朋友建議去讀一下,跑一跑測試用例。
我這裏只截取一個結論給你們看,證實書上是沒有胡說的:
這本書分別從原理和應用的角度去講學了字節碼,裏面有很是多的實戰內容。仍是很不錯的。
推薦這本書,本文我會送出五本,參與方式下一小節說。
而後推薦一下樑飛大佬的博客,好久沒有更新了,可是仍是有不少乾貨的,建議有時間的能夠去翻一翻:
https://www.iteye.com/category/7506
文中提到的《深刻理解JVM字節碼》我也聯繫到了機械工業出版社的工做人員,出版社決定給我這個小小號主贊助五本書。我固然會回饋給讀者朋友們啦。
還有,在其餘平臺收到一個評論說個人文章裏面的表情包是花裏胡哨的東西。其實我已經很剋制的在加表情包了。
沙雕表情包頗有趣的,哈哈哈哈。
另外,最近微信公衆號改版,對我這樣的小號主能夠說是很是打擊了。閱讀量直線降低,正反饋持續減弱。
因此安排個「一鍵三連」(轉發、在看、點贊)吧,周更很累的,不要白嫖我,須要一點正反饋。才疏學淺,不免會有紕漏,若是你發現了錯誤的地方,因爲本號沒有留言功能,還請你在後臺留言指出來,我對其加以修改。
感謝您的閱讀,我堅持原創,十分歡迎並感謝您的關注。
我是 why,一個被代碼耽誤的文學創做者,不是大佬,可是喜歡分享,是一個又暖又有料的四川好男人。
還有,重要的事情說三遍:
歡迎關注我呀。歡迎關注我呀。歡迎關注我呀。