Dubbo 自定義異常,你是怎麼處理的?

張少林同學發表於2019-01-06

Dubbo 自定義異常,你是怎麼處理的?

前言

記錄Dubbo對於自定義異常的處理方式.

實現目標

  • 服務層異常,直接向上層丟擲,web層統一捕獲處理
  • 如果是系統自定義異常,則返回{"code":xxx,"msg":yyy} 其中code對應為錯誤碼msg對應為異常資訊
  • 如果非系統自定義異常,返回{"code":-1,"msg":"未知錯誤"},同時將異常堆疊資訊輸出到日誌,便於定位問題

專案架構

先來張系統架構圖吧,這張圖來源自網路,相信現在大部分中小企業的分散式叢集架構都是類似這樣的設計:

Dubbo 自定義異常,你是怎麼處理的?

簡要說明下分層架構:

  • 通常情況下會有專門一臺堡壘機做統一的代理轉發,客戶端(pc,移動端等)訪問由nginx統一暴露的入口
  • nginx反向代理,負載均衡到web伺服器,由tomcat組成的叢集,web層僅僅是作為介面請求的入口,沒有實際的業務邏輯
  • web層再用rpc遠端呼叫註冊到zookeeperdubbo服務叢集,dubbo服務與資料層互動,處理業務邏輯

前後端分離,使用json格式做資料互動,格式可以統一如下:

    {
    	"code": 200,            //狀態碼:200成功,其他為失敗
    	"msg": "success",       //訊息,成功為success,其他為失敗原因
    	"data": object     //具體的資料內容,可以為任意格式
    }
複製程式碼

對映為javabean可以統一定義為:

/**
 * @program: easywits
 * @description: http請求 返回的最外層物件
 * @author: zhangshaolin
 * @create: 2018-04-27 10:43
 **/
@Data
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
public class BaseResult<T> implements Serializable{

    private static final long serialVersionUID = -6959952431964699958L;

    /**
     * 狀態碼:200成功,其他為失敗
     */
    public Integer code;

    /**
     * 成功為success,其他為失敗原因
     */
    public String msg;

    /**
     * 具體的內容
     */
    public T data;
}
複製程式碼

返回結果工具類封裝:

/**
 * @program: easywits
 * @description: http返回結果工具類
 * @author: zhangshaolin
 * @create: 2018-07-14 13:38
 **/
public class ResultUtil {

    /**
     * 訪問成功時呼叫 包含data
     * @param object
     * @return
     */
    public static BaseResult success(Object object){
        BaseResult result = new BaseResult();
        result.setCode(200);
        result.setMsg("success");
        result.setData(object);
        return result;
    }

    /**
     * 訪問成功時呼叫 不包含data
     * @return
     */
    public static BaseResult success(){
        return success(null);
    }

    /**
     * 返回異常情況 不包含data
     * @param code
     * @param msg
     * @return
     */
    public static BaseResult error(Integer code,String msg){
        BaseResult result = new BaseResult();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }
    
     /**
     * 返回異常情況 包含data
     * @param resultEnum 結果列舉類&emsp;統一管理&emsp;code msg
     * @param object
     * @return
     */
    public static BaseResult error(ResultEnum resultEnum,Object object){
        BaseResult result = error(resultEnum);
        result.setData(object);
        return result;
    }

    /**
     * 全域性基類自定義異常 異常處理
     * @param e
     * @return
     */
    public static BaseResult error(BaseException e){
        return error(e.getCode(),e.getMessage());
    }

    /**
     * 返回異常情況 不包含data
     * @param resultEnum 結果列舉類&emsp;統一管理&emsp;code msg
     * @return
     */
    public static BaseResult error(ResultEnum resultEnum){
        return error(resultEnum.getCode(),resultEnum.getMsg());
    }
}
複製程式碼

因此,模擬一次前端呼叫請求的過程可以如下:

  • web層介面

    @RestController
    @RequestMapping(value = "/user")
    public class UserController {
    
        @Autowired
        UserService mUserService;
        
        @Loggable(descp = "使用者個人資料", include = "")
        @GetMapping(value = "/info")
        public BaseResult userInfo() {
            return mUserService.userInfo();
        }
    }
    複製程式碼
  • 服務層介面

     @Override
    public BaseResult userInfo() {
        UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
        UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId());
        return ResultUtil.success(userInfoVo);
    }
    複製程式碼

自定義系統異常

定義一個自定義異常,用於手動丟擲異常資訊,注意這裡基礎RuntimeException未受檢異常

簡單說明,RuntimeException及其子類為未受檢異常,其他異常為受檢異常,未受檢異常是執行時丟擲的異常,而受檢異常則在編譯時則強則報錯

public class BaseException extends RuntimeException{

    private Integer code;

    public BaseException() {
    }

    public BaseException(ResultEnum resultEnum) {
        super(resultEnum.getMsg());
        this.code = resultEnum.getCode();
    }
    ...省略set get方法
}
複製程式碼

為了方便對結果統一管理,定義一個結果列舉類:

public enum ResultEnum {
    UNKNOWN_ERROR(-1, "o(╥﹏╥)o~~系統出異常啦!,請聯絡管理員!!!"),
    SUCCESS(200, "success");
    
    private Integer code;
    
    private String msg;

    ResultEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}
複製程式碼

web層統一捕獲異常

定義BaseController抽象類,統一捕獲由服務層丟擲的異常,所有新增Controller繼承該類即可。

public abstract class BaseController {
    private final static Logger LOGGER = LoggerFactory.getLogger(BaseController.class);
    
       /**
     * 統一異常處理
     *
     * @param e
     */
    @ExceptionHandler()
    public Object exceptionHandler(Exception e) {
        if (e instanceof BaseException) {
            //全域性基類自定義異常,返回{code,msg}
            BaseException baseException = (BaseException) e;
            return ResultUtil.error(baseException);
        } else {
            LOGGER.error("系統異常: {}", e);
            return ResultUtil.error(ResultEnum.UNKNOWN_ERROR);
        }
    }
}
複製程式碼

驗證

  1. 以上web層介面UserController繼承BaseController,統一捕獲異常

  2. 服務層假設丟擲自定義系統異常BaseException,程式碼如下:

     @Override
     public BaseResult userInfo() {
        UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
        UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId());
          if (userInfoVo != null) {
            //這裡假設拋個自定義異常,返回結果{code:10228 msg:"使用者存在!"}
            throw new BaseException(ResultEnum.USER_EXIST);
        }
        return ResultUtil.success(userInfoVo);
    }
    複製程式碼

然而呼叫結果後,上層捕獲到的異常卻不是BaseException,而被認為了未知錯誤丟擲了.帶著疑問看看Dubbo對於異常的處理

Dubbo異常處理

Dubbo對於異常有統一的攔截處理,以下是Dubbo異常攔截器主要程式碼:

 @Override
    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();

                    // directly throw if it's checked exception
                    // 如果是checked異常,直接丟擲
                    if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
                        return result;
                    }
                    // directly throw if the exception appears in the signature
                    // 在方法簽名上有宣告,直接丟擲
                    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 日誌
                    // for the exception not found in method's signature, print ERROR message in server's log.
                    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 包裡,直接丟擲
                    // directly throw if exception class and interface class are in the same jar file.
                    String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
                    String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
                    if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
                        return result;
                    }
                    // 是JDK自帶的異常,直接丟擲
                    // directly throw if it's JDK exception
                    String className = exception.getClass().getName();
                    if (className.startsWith("java.") || className.startsWith("javax.")) {
                        return result;
                    }
                    // 是Dubbo本身的異常,直接丟擲
                    // directly throw if it's dubbo exception
                    if (exception instanceof RpcException) {
                        return result;
                    }

                    // 否則,包裝成RuntimeException拋給客戶端
                    // otherwise, wrap with RuntimeException and throw back to the client
                    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;
        }
    }
複製程式碼

簡要說明:

  • 有異常,並且非泛化呼叫時,如果是受檢異常,則直接丟擲
  • 有異常,並且非泛化呼叫時,在方法簽名上有宣告,則直接丟擲
  • 有異常,並且非泛化呼叫時,異常類和介面類在同一 jar 包裡,則直接丟擲
  • 有異常,並且非泛化呼叫時,是Dubbo本身的異常(RpcException),則直接丟擲
  • 有異常,並且非泛化呼叫時,剩下的情況,全部都會包裝成RuntimeException拋給客戶端

到現在問題很明顯了,我們自定義的BaseException未受檢異常,況且不符合Dubbo異常攔截器中直接丟擲的要求,Dubbo將其包裝成了RuntimeException,所以在上層BaseController中統一捕獲為系統未知錯誤了.

解決辦法

  • 異常類BaseException和介面類在同一 jar 包裡,但是這種方式要在每個jar中放置一個異常類,不好統一維護管理
  • 在介面方法簽名上顯式宣告丟擲BaseException,這種方式相對簡單一些,比較好統一維護,只是每個介面都要顯式宣告一下異常罷了,這裡我選擇這種方式解決

問題

為什麼一定要丟擲自定義異常來中斷程式執行,用return ResultUtil.error(ResultEnum resultEnum) 強制返回{code:xxx msg:xxx}結果,不是一樣可以強制中斷程式執行?

玩過Spring的肯定都知道,Spring喲宣告式事物的概念,即在介面中新增事物註解,當發生異常時,全部介面執行事物回滾..看下方的虛擬碼:

@Transactional(rollbackFor = Exception.class)
public BaseResult handleData(){
    
    //1. 運算元據庫,新增資料表A一條資料,返回新增資料主鍵id
    
    //2. 運算元據庫,新增資料庫B一條資料,以資料表A主鍵id為外來鍵關聯
    
    //3. 執行成功&emsp;返回結果
}
複製程式碼
  • 該介面宣告瞭異常事物回滾,傳送異常時會全部回滾
  • 步驟1資料入庫失敗,理論上是拿不到主鍵id的,此時應當丟擲自定義異常,提示操作失敗
  • 如果步驟1資料入庫成功,步驟2中資料入庫失敗,那麼理論上步驟1中的資料應當也要回滾,如果此時強制返回異常結果,那麼步驟1入庫資料則成為髒資料,此時丟擲自定義異常是最合理的

最後的思考

在實際問題場景中去閱讀原始碼是最合適的,帶著問題有目的的看指定原始碼會讓人有豁然開朗的感覺.

更多原創文章會第一時間推送公眾號【張少林同學】,歡迎關注!

Dubbo 自定義異常,你是怎麼處理的?

相關文章