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

前言

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

實現目標

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

項目架構

先來張系統架構圖吧,這張圖來源自網絡,相信如今大部分中小企業的分佈式集羣架構都是相似這樣的設計:java

簡要說明下分層架構:nginx

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

先後端分離,使用json格式作數據交互,格式能夠統一以下:web

{
    	"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;
}
複製代碼

返回結果工具類封裝:json

/** * @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層接口tomcat

    @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入庫數據則成爲髒數據,此時拋出自定義異常是最合理的

最後的思考

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

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

相關文章
相關標籤/搜索