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


前言

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

實現目標

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

項目架構

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

簡要說明下分層架構:

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

前后端分離,使用json格式做數據交互,格式可以統一如下:

1    {
2        "code"200,            //狀態碼:200成功,其他為失敗
3        "msg""success",       //消息,成功為success,其他為失敗原因
4        "data": object     //具體的數據內容,可以為任意格式
5    }

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

 1/**
2 * @program: easywits
3 * @description: http請求 返回的最外層對象
4 * @author: zhangshaolin
5 * @create: 2018-04-27 10:43
6 **/

7@Data
8@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
9public class BaseResult<Timplements Serializable{
10
11    private static final long serialVersionUID = -6959952431964699958L;
12
13    /**
14     * 狀態碼:200成功,其他為失敗
15     */

16    public Integer code;
17
18    /**
19     * 成功為success,其他為失敗原因
20     */

21    public String msg;
22
23    /**
24     * 具體的內容
25     */

26    public T data;
27}

返回結果工具類封裝:

 1/**
2 * @program: easywits
3 * @description: http返回結果工具類
4 * @author: zhangshaolin
5 * @create: 2018-07-14 13:38
6 **/

7public class ResultUtil {
8
9    /**
10     * 訪問成功時調用 包含data
11     * @param object
12     * @return
13     */

14    public static BaseResult success(Object object){
15        BaseResult result = new BaseResult();
16        result.setCode(200);
17        result.setMsg("success");
18        result.setData(object);
19        return result;
20    }
21
22    /**
23     * 訪問成功時調用 不包含data
24     * @return
25     */

26    public static BaseResult success(){
27        return success(null);
28    }
29
30    /**
31     * 返回異常情況 不包含data
32     * @param code
33     * @param msg
34     * @return
35     */

36    public static BaseResult error(Integer code,String msg){
37        BaseResult result = new BaseResult();
38        result.setCode(code);
39        result.setMsg(msg);
40        return result;
41    }
42
43     /**
44     * 返回異常情況 包含data
45     * @param resultEnum 結果枚舉類 統一管理 code msg
46     * @param object
47     * @return
48     */

49    public static BaseResult error(ResultEnum resultEnum,Object object){
50        BaseResult result = error(resultEnum);
51        result.setData(object);
52        return result;
53    }
54
55    /**
56     * 全局基類自定義異常 異常處理
57     * @param e
58     * @return
59     */

60    public static BaseResult error(BaseException e){
61        return error(e.getCode(),e.getMessage());
62    }
63
64    /**
65     * 返回異常情況 不包含data
66     * @param resultEnum 結果枚舉類 統一管理 code msg
67     * @return
68     */

69    public static BaseResult error(ResultEnum resultEnum){
70        return error(resultEnum.getCode(),resultEnum.getMsg());
71    }
72}

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

  • web層接口

     1@RestController
    2@RequestMapping(value = "/user")
    3public class UserController {
    4    @Autowired
    5    UserService mUserService;
    6    @Loggable(descp = "用戶個人資料", include = "")
    7    @GetMapping(value = "/info")
    8    public BaseResult userInfo() {
    9        return mUserService.userInfo();
    10    }
    11}
  • 服務層接口

    1 @Override
    2public BaseResult userInfo() {
    3    UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
    4    UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId());
    5    return ResultUtil.success(userInfoVo);
    6}

自定義系統異常

定義一個自定義異常,用於手動拋出異常信息,注意這里基礎RuntimeException未受檢異常

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

 1public class BaseException extends RuntimeException{
2
3    private Integer code;
4
5    public BaseException() {
6    }
7
8    public BaseException(ResultEnum resultEnum) {
9        super(resultEnum.getMsg());
10        this.code = resultEnum.getCode();
11    }
12    ...省略set get方法
13}

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

 1public enum ResultEnum {
2    UNKNOWN_ERROR(-1"o(╥﹏╥)o~~系統出異常啦!,請聯系管理員!!!"),
3    SUCCESS(200"success");
4
5    private Integer code;
6
7    private String msg;
8
9    ResultEnum(Integer code, String msg) {
10        this.code = code;
11        this.msg = msg;
12    }
13}

`web`層統一捕獲異常

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

 1public abstract class BaseController {
2    private final static Logger LOGGER = LoggerFactory.getLogger(BaseController.class);
3
4       /**
5     * 統一異常處理
6     *
7     * @param e
8     */

9    @ExceptionHandler()
10    public Object exceptionHandler(Exception e) {
11        if (e instanceof BaseException) {
12            //全局基類自定義異常,返回{code,msg}
13            BaseException baseException = (BaseException) e;
14            return ResultUtil.error(baseException);
15        } else {
16            LOGGER.error("系統異常: {}", e);
17            return ResultUtil.error(ResultEnum.UNKNOWN_ERROR);
18        }
19    }
20}

驗證

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

  2. 服務層假設拋出自定義系統異常BaseException,代碼如下:

     1 @Override
    2 public BaseResult userInfo() {
    3    UserInfo userInfo = ThreadLocalUtil.getInstance().getUserInfo();
    4    UserInfoVo userInfoVo = getUserInfoVo(userInfo.getUserId());
    5      if (userInfoVo != null) {
    6        //這里假設拋個自定義異常,返回結果{code:10228 msg:"用戶存在!"}
    7        throw new BaseException(ResultEnum.USER_EXIST);
    8    }
    9    return ResultUtil.success(userInfoVo);
    10}

然而調用結果后,上層捕獲到的異常卻不是BaseException,而被認為了未知錯誤拋出了.帶着疑問看看Dubbo對於異常的處理

Dubbo異常處理

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

 1 @Override
2    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
3        try {
4            // 服務調用
5            Result result = invoker.invoke(invocation);
6            // 有異常,並且非泛化調用
7            if (result.hasException() && GenericService.class != invoker.getInterface()) {
8                try {
9                    Throwable exception = result.getException();
10
11                    // directly throw if it's checked exception
12                    // 如果是checked異常,直接拋出
13                    if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
14                        return result;
15                    }
16                    // directly throw if the exception appears in the signature
17                    // 在方法簽名上有聲明,直接拋出
18                    try {
19                        Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
20                        Class<?>[] exceptionClassses = method.getExceptionTypes();
21                        for (Class<?> exceptionClass : exceptionClassses) {
22                            if (exception.getClass().equals(exceptionClass)) {
23                                return result;
24                            }
25                        }
26                    } catch (NoSuchMethodException e) {
27                        return result;
28                    }
29
30                    // 未在方法簽名上定義的異常,在服務器端打印 ERROR 日志
31                    // for the exception not found in method's signature, print ERROR message in server's log.
32                    logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()
33                            + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
34                            + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);
35
36                    // 異常類和接口類在同一 jar 包里,直接拋出
37                    // directly throw if exception class and interface class are in the same jar file.
38                    String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
39                    String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
40                    if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
41                        return result;
42                    }
43                    // 是JDK自帶的異常,直接拋出
44                    // directly throw if it's JDK exception
45                    String className = exception.getClass().getName();
46                    if (className.startsWith("java.") || className.startsWith("javax.")) {
47                        return result;
48                    }
49                    // 是Dubbo本身的異常,直接拋出
50                    // directly throw if it's dubbo exception
51                    if (exception instanceof RpcException) {
52                        return result;
53                    }
54
55                    // 否則,包裝成RuntimeException拋給客戶端
56                    // otherwise, wrap with RuntimeException and throw back to the client
57                    return new RpcResult(new RuntimeException(StringUtils.toString(exception)));
58                } catch (Throwable e) {
59                    logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost()
60                            + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
61                            + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
62                    return result;
63                }
64            }
65            // 返回
66            return result;
67        } catch (RuntimeException e) {
68            logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost()
69                    + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName()
70                    + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
71            throw e;
72        }
73    }

簡要說明:

  • 有異常,並且非泛化調用時,如果是受檢異常,則直接拋出
  • 有異常,並且非泛化調用時,在方法簽名上有聲明,則直接拋出
  • 有異常,並且非泛化調用時,異常類和接口類在同一 jar 包里,則直接拋出
  • 有異常,並且非泛化調用時,是Dubbo本身的異常(RpcException),則直接拋出
  • 有異常,並且非泛化調用時,剩下的情況,全部都會包裝成RuntimeException拋給客戶端

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

解決辦法

  • 異常類BaseException和接口類在同一 jar 包里,但是這種方式要在每個jar中放置一個異常類,不好統一維護管理
  • 在接口方法簽名上顯式聲明拋出BaseException,這種方式相對簡單一些,比較好統一維護,只是每個接口都要顯式聲明一下異常罷了,這里我選擇這種方式解決

問題

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

玩過Spring的肯定都知道,Spring喲聲明式事物的概念,即在接口中添加事物注解,當發生異常時,全部接口執行事物回滾..看下方的偽代碼:

1@Transactional(rollbackFor = Exception.class)
2public BaseResult handleData(){
3
4    //1. 操作數據庫,新增數據表A一條數據,返回新增數據主鍵id
5
6    //2. 操作數據庫,新增數據庫B一條數據,以數據表A主鍵id為外鍵關聯
7
8    //3. 執行成功 返回結果
9}
  • 該接口聲明了異常事物回滾,發送異常時會全部回滾
  • 步驟1數據入庫失敗,理論上是拿不到主鍵id的,此時應當拋出自定義異常,提示操作失敗
  • 如果步驟1數據入庫成功,步驟2中數據入庫失敗,那么理論上步驟1中的數據應當也要回滾,如果此時強制返回異常結果,那么步驟1入庫數據則成為臟數據,此時拋出自定義異常是最合理的

最后的思考

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM