Dubbo異常處理源碼探究及其最佳實(shí)踐
后臺(tái)回復(fù)“Java”即可獲贈(zèng)Java學(xué)習(xí)資料
1 背景
在日常業(yè)務(wù)開發(fā)過程中,我們?yōu)榱俗寴I(yè)務(wù)代碼更健壯,遇到錯(cuò)誤時(shí)返回的提示更友好,一般會(huì)自定義一些業(yè)務(wù)異常。根據(jù)業(yè)務(wù)需要,分為自定義受檢異常和非受檢異常
知識(shí)點(diǎn)回顧
Exception類及其子類,但不包括RuntimeException的子類,統(tǒng)稱為受檢異常。如果方法執(zhí)行過程中有可能拋出此類異常,必須在方法簽名上聲明
RuntimeException類及其子類,統(tǒng)稱為非受檢異常。如果方法執(zhí)行過程中有可能拋出此類異常,可以不必在方法簽名上聲明
課代表所負(fù)責(zé)的項(xiàng)目使用SpringCloudAlibaba落地了微服務(wù),開發(fā)中組內(nèi)兄弟遇到一個(gè)問題:Dubbo RPC調(diào)用時(shí),provider拋出的一個(gè)業(yè)務(wù)類非受檢異常,consumer接到時(shí)卻是RuntimeException 并且message被和堆棧信息拼接到了一起。
2 問題復(fù)現(xiàn)
Dubbo 微服務(wù)中,provider 分為api和service,consumer只需要引入 api從注冊(cè)中心調(diào)用service 實(shí)例即可。
當(dāng)service 中拋出一個(gè)自定義的非受檢異常,且其相應(yīng)api包中沒有這個(gè)異常類時(shí),就會(huì)出現(xiàn)異常被包裝為RuntimeException的情況。
其實(shí)問題分析到這里,基本就有眉目了:Dubbo是一個(gè)RPC框架,客戶端調(diào)用的都是遠(yuǎn)程方法,參數(shù)和返回值都是經(jīng)過序列化和反序列化為字節(jié)數(shù)組傳輸?shù)摹?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">consumer必須認(rèn)識(shí)這個(gè)異常才能反序列化成功。
很明顯,我們拋的這個(gè)異常 Dubbo認(rèn)為consumer不認(rèn)識(shí),為了避免反序列化失敗,從而對(duì)異常進(jìn)行了包裝。
下面結(jié)合源碼闡述Dubbo的異常處理機(jī)制。
3 源碼分析
Dubbo 遠(yuǎn)程調(diào)用的異常由ExceptionFilter類處理
public?Result?invoke(Invoker>?invoker,?Invocation?invocation)?throws?RpcException?{
????????try?{
????????????Result?result?=?invoker.invoke(invocation);
????????????if?(result.hasException()?&&?GenericService.class?!=?invoker.getInterface())?{
????????????????try?{
????????????????????Throwable?exception?=?result.getException();
????????????????????//?如果是checked異常,直接拋出
????????????????????if?(!?(exception?instanceof?RuntimeException)?&&?(exception?instanceof?Exception))?{
????????????????????????return?result;
????????????????????}
????????????????????//?在方法簽名上有聲明,直接拋出
????????????????????try?{
????????????????????????Method?method?=?invoker.getInterface().getMethod(invocation.getMethodName(),?invocation.getParameterTypes());
????????????????????????Class>[]?exceptionClassses?=?method.getExceptionTypes();
????????????????????????for?(Class>?exceptionClass?:?exceptionClassses)?{
????????????????????????????if?(exception.getClass().equals(exceptionClass))?{
????????????????????????????????return?result;
????????????????????????????}
????????????????????????}
????????????????????}?catch?(NoSuchMethodException?e)?{
????????????????????????return?result;
????????????????????}
????????????????????//?未在方法簽名上定義的異常,在服務(wù)器端打印ERROR日志
????????????????????logger.error("Got?unchecked?and?undeclared?exception?which?called?by?"?+?RpcContext.getContext().getRemoteHost()
????????????????????????????+?".?service:?"?+?invoker.getInterface().getName()?+?",?method:?"?+?invocation.getMethodName()
????????????????????????????+?",?exception:?"?+?exception.getClass().getName()?+?":?"?+?exception.getMessage(),?exception);
????????????????????//?異常類和接口類在同一jar包里,直接拋出
????????????????????String?serviceFile?=?ReflectUtils.getCodeBase(invoker.getInterface());
????????????????????String?exceptionFile?=?ReflectUtils.getCodeBase(exception.getClass());
????????????????????if?(serviceFile?==?null?||?exceptionFile?==?null?||?serviceFile.equals(exceptionFile)){
????????????????????????return?result;
????????????????????}
????????????????????//?是JDK自帶的異常,直接拋出
????????????????????String?className?=?exception.getClass().getName();
????????????????????if?(className.startsWith("java.")?||?className.startsWith("javax."))?{
????????????????????????return?result;
????????????????????}
????????????????????//?是Dubbo本身的異常,直接拋出
????????????????????if?(exception?instanceof?RpcException)?{
????????????????????????return?result;
????????????????????}
????????????????????//?否則,包裝成RuntimeException拋給客戶端
????????????????????return?new?RpcResult(new?RuntimeException(StringUtils.toString(exception)));
????????????????}?catch?(Throwable?e)?{
????????????????????logger.warn("Fail?to?ExceptionFilter?when?called?by?"?+?RpcContext.getContext().getRemoteHost()
????????????????????????????+?".?service:?"?+?invoker.getInterface().getName()?+?",?method:?"?+?invocation.getMethodName()
????????????????????????????+?",?exception:?"?+?e.getClass().getName()?+?":?"?+?e.getMessage(),?e);
????????????????????return?result;
????????????????}
????????????}
????????????return?result;
????????}?catch?(RuntimeException?e)?{
????????????logger.error("Got?unchecked?and?undeclared?exception?which?called?by?"?+?RpcContext.getContext().getRemoteHost()
????????????????????+?".?service:?"?+?invoker.getInterface().getName()?+?",?method:?"?+?invocation.getMethodName()
????????????????????+?",?exception:?"?+?e.getClass().getName()?+?":?"?+?e.getMessage(),?e);
????????????throw?e;
????????}
通過源碼可以看到,該類的主要功能是返回接口拋出的異常,Dubbo將其定義為如下幾種情況:
如果是 checked異常,直接拋出在方法簽名上有聲明,直接拋出 不符合1,2 的被認(rèn)為是錯(cuò)誤,會(huì)打印error日志,并嘗試如下處理: 異常類和接口類在同一個(gè) jar包里,直接拋出是 JDK自帶的異常,直接拋出是 Dubbo本身的異常,直接拋出否則,包裝成 RuntimeException拋給客戶端
事實(shí)上Dubbo作為RPC框架已經(jīng)把各種拋異常的情況都考慮全了,最后如果Dubbo認(rèn)為consumer不認(rèn)識(shí)這個(gè)異常還會(huì)包裝成RuntimeException兜底,防止反序列化失敗。
如果發(fā)生了consumer找不到provider所拋異常的這種情況,不客氣地講,一定是開發(fā)者的問題,把這個(gè)歸罪于Dubbo 那可就太冤枉它了!
4 最佳實(shí)踐
Dubbo官網(wǎng)->Dubbo 2.7->用戶文檔->服務(wù)化最佳實(shí)踐 中有如下描述:
分包
建議將服務(wù)接口、服務(wù)模型、服務(wù)異常等均放在 API 包中,因?yàn)榉?wù)模型和異常也是 API 的一部分,這樣做也符合分包原則:重用發(fā)布等價(jià)原則(REP),共同重用原則(CRP)。
所以,符合Dubbo 最佳實(shí)踐的provider-api中應(yīng)該包含服務(wù)接口包,服務(wù)模型包,服務(wù)異常包。所有service中用到的異常,都應(yīng)該在api包中聲明,這樣consumer調(diào)用時(shí)才會(huì)符合Dubbo 要求的:
異常類和接口類在同一個(gè)
jar包里,直接拋出
從而避免被Dubbo 包裝成RuntimeException拋給客戶端。
所以,針對(duì)文章開頭遇到的問題,我們只需要把provider-service中拋出自定義的非受檢異常 在provider-api中定義,同時(shí)在相應(yīng)的方法上throw出來就可以了,這樣既可以防止被Dubbo包裝,也不會(huì)因?yàn)榉椒ê灻袥]聲明異常而導(dǎo)致Dubbo報(bào)error錯(cuò)誤。而且,因?yàn)槭欠鞘軝z異常,所以也不強(qiáng)制客戶端對(duì)方法進(jìn)行try catch。
一個(gè)可參考的分包實(shí)踐:
??+-?scr
??????|
??????+-?demo
??????????|
??????????+-?domain?(業(yè)務(wù)域內(nèi)傳輸數(shù)據(jù)用的?DTO)
??????????|
??????????+-?service?(API?中?service?接口的實(shí)現(xiàn)類)
??????????|
??????????+-?exception?(業(yè)務(wù)域中的自定義異常)
5 彎路
如果 Google 關(guān)鍵字 [Dubbo 異常處理],你會(huì)發(fā)現(xiàn)幾乎所有文章都是下面這幾個(gè)思路:
自定義一個(gè) ExceptionFilter讓Dubbo使用,兼容自己的業(yè)務(wù)異常類在provider 端寫個(gè)AOP攔截所有異常自己處理 把 unchecked異常改為checked異常
當(dāng)然,上面這些方法完全可以解決問題,但這是不是有殺雞用牛刀的意思?
明明是代碼開發(fā)不規(guī)范,沒有遵循最佳實(shí)踐,卻要強(qiáng)行歸罪于底層框架。Dubbo在努力做得通用,而上面的處理方式卻在讓代碼變得緊耦合。
總結(jié)問題本質(zhì):Dubbo在認(rèn)為consumer找不到異常類時(shí),為了防止發(fā)生反序列化失敗,對(duì)異常進(jìn)行了一層包裝。針對(duì)這一實(shí)質(zhì),我們用最簡(jiǎn)單、高效,影響最小的辦法解決就可以了。
課代表相信讀者結(jié)合Dubbo 異常處理的源碼,應(yīng)該會(huì)有自己的判斷。
6 反思
遇事不決問Google,多數(shù)情況下我們遇到的問題都會(huì)搜到答案,對(duì)于同樣一個(gè)問題,解決的方法可能多種多樣,我們需要做的是找到問題的本質(zhì),舉一反三,根據(jù)自己業(yè)務(wù)的實(shí)際情況選擇最合適的解決方案。
切勿盲從,須知:盡信書不如無(wú)書。
-------------------?End?-------------------
往期精彩文章推薦:
手把手帶你用Java打造一款對(duì)對(duì)碰游戲(上篇)
手把手帶你用Java打造一款對(duì)對(duì)碰游戲(下篇)
手把手帶你用Java實(shí)現(xiàn)點(diǎn)燈游戲(上篇)
手把手帶你用Java實(shí)現(xiàn)點(diǎn)燈游戲(下篇)

歡迎大家點(diǎn)贊,留言,轉(zhuǎn)發(fā),轉(zhuǎn)載,感謝大家的相伴與支持
想加入Python學(xué)習(xí)群請(qǐng)?jiān)诤笈_(tái)回復(fù)【入群】
萬(wàn)水千山總是情,點(diǎn)個(gè)【在看】行不行
