Dubbo異常處理源碼探究及其最佳實踐

Hi,你們好。我是Java課表明。今天咱們來講一說Dubbo。java

推薦語:Dubbo做爲一款高性能的 RPC 框架,在微服務架構中普遍應用,本文基於開發過程當中的一次異常處理,深刻剖析了Dubbo 的異常處理邏輯,並結合源碼,給出了 Dubbo 異常處理的最佳實踐。

1 背景

在平常業務開發過程當中,咱們爲了讓業務代碼更健壯,遇到錯誤時返回的提示更友好,通常會自定義一些業務異常。根據業務須要,分爲自定義受檢異常和非受檢異常apache

知識點回顧segmentfault

Exception類及其子類,但不包括 RuntimeException 的子類,統稱爲受檢異常。若是方法執行過程當中有可能拋出此類異常,必須在方法簽名上聲明api

RuntimeException 類及其子類,統稱爲非受檢異常。若是方法執行過程當中有可能拋出此類異常,能夠沒必要在方法簽名上聲明數組

課表明所負責的項目使用SpringCloudAlibaba落地了微服務,開發中組內兄弟遇到一個問題:Dubbo RPC調用時,provider拋出的一個業務類非受檢異常,consumer接到時倒是RuntimeException 而且message被和堆棧信息拼接到了一塊兒。服務器

2 問題復現

Dubbo 微服務中,provider 分爲apiserviceconsumer只須要引入 api 從註冊中心調用service 實例便可。架構

service 中拋出一個自定義的非受檢異常,且其相應api包中沒有這個異常類時,就會出現異常被包裝爲RuntimeException的狀況。框架

其實問題分析到這裏,基本就有眉目了:Dubbo是一個RPC框架,客戶端調用的都是遠程方法,參數和返回值都是通過序列化和反序列化爲字節數組傳輸的。consumer必須認識這個異常才能反序列化成功。dom

很明顯,咱們拋的這個異常 Dubbo認爲consumer不認識,爲了不反序列化失敗,從而對異常進行了包裝。ide

下面結合源碼闡述Dubbo的異常處理機制。

3 源碼分析

Dubbo 遠程調用的異常由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;
                    }

                    // 未在方法簽名上定義的異常,在服務器端打印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將其定義爲以下幾種狀況:

  1. 若是是checked異常,直接拋出
  2. 在方法簽名上有聲明,直接拋出
  3. 不符合1,2 的被認爲是錯誤,會打印error日誌,並嘗試以下處理:

    • 異常類和接口類在同一個jar包裏,直接拋出
    • JDK自帶的異常,直接拋出
    • Dubbo自己的異常,直接拋出
    • 不然,包裝成RuntimeException拋給客戶端

事實上Dubbo做爲RPC框架已經把各類拋異常的狀況都考慮全了,最後若是Dubbo認爲consumer不認識這個異常還會包裝成RuntimeException兜底,防止反序列化失敗。

若是發生了consumer找不到provider所拋異常的這種狀況,不客氣地講,必定是開發者的問題,把這個歸罪於Dubbo 那可就太冤枉它了!

4 最佳實踐

Dubbo官網->Dubbo 2.7->用戶文檔->服務化最佳實踐 中有以下描述:

分包

建議將服務接口、服務模型、服務異常等均放在 API 包中,由於服務模型和異常也是 API 的一部分,這樣作也符合分包原則:重用發佈等價原則(REP),共同重用原則(CRP)。

因此,符合Dubbo 最佳實踐的provider-api中應該包含服務接口包,服務模型包,服務異常包。全部service中用到的異常,都應該在api包中聲明,這樣consumer調用時纔會符合Dubbo 要求的:

異常類和接口類在同一個 jar包裏,直接拋出

從而避免被Dubbo 包裝成RuntimeException拋給客戶端。

因此,針對文章開頭遇到的問題,咱們只須要把provider-service中拋出自定義的非受檢異常 在provider-api中定義,同時在相應的方法上throw出來就能夠了,這樣既能夠防止被Dubbo包裝,也不會由於方法簽名中沒聲明異常而致使Dubboerror錯誤。並且,由於是非受檢異常,因此也不強制客戶端對方法進行try catch

一個可參考的分包實踐:

+- scr
      |
      +- demo
          |
          +- domain (業務域內傳輸數據用的 DTO)
          |
          +- service (API 中 service 接口的實現類)
          |
          +- exception (業務域中的自定義異常)

5 彎路

若是 Google 關鍵字 [Dubbo 異常處理],你會發現幾乎全部文章都是下面這幾個思路:

  1. 自定義一個ExceptionFilterDubbo使用,兼容本身的業務異常類
  2. 在provider 端寫個AOP攔截全部異常本身處理
  3. unchecked異常改成checked異常

固然,上面這些方法徹底能夠解決問題,但這是否是有殺雞用牛刀的意思?

明明是代碼開發不規範,沒有遵循最佳實踐,卻要強行歸罪於底層框架。Dubbo在努力作得通用,而上面的處理方式卻在讓代碼變得緊耦合。

總結問題本質:Dubbo在認爲consumer找不到異常類時,爲了防止發生反序列化失敗,對異常進行了一層包裝。針對這一實質,咱們用最簡單、高效,影響最小的辦法解決就能夠了。

課表明相信讀者結合Dubbo 異常處理的源碼,應該會有本身的判斷。

6 反思

遇事不決問Google,多數狀況下咱們遇到的問題都會搜到答案,對於一樣一個問題,解決的方法可能多種多樣,咱們須要作的是找到問題的本質,觸類旁通,根據本身業務的實際狀況選擇最合適的解決方案。

切勿盲從,須知:盡信書不如無書。


【推薦閱讀】
RabbitMQ官方教程譯文
Freemarker 教程(一)-模板開發手冊
使用Spring Validation優雅地校驗參數
下載的附件名總亂碼?你該去讀一下 RFC 文檔了!
深刻淺出 MySQL 優先隊列(你必定會踩到的order by limit 問題)


碼字不易,歡迎點贊關注和分享。
搜索:【Java課表明】,關注公衆號,每日一更,及時獲取更多Java乾貨。

相關文章
相關標籤/搜索