摘要:前言記錄對于自定義異常的處理方式實現目標服務層異常,直接向上層拋出,層統一捕獲處理如果是系統自定義異常,則返回其中對應為錯誤碼,對應為異常信息如果非系統自定義異常,返回未知錯誤,同時將異常堆棧信息輸出到日志便于定位問題項目架構先來張系統架
前言
記錄Dubbo對于自定義異常的處理方式.
實現目標服務層異常,直接向上層拋出,web層統一捕獲處理
如果是系統自定義異常,則返回{"code":xxx,"msg":yyy} 其中code對應為錯誤碼,msg對應為異常信息
如果非系統自定義異常,返回{"code":-1,"msg":"未知錯誤"},同時將異常堆棧信息輸出到日志,便于定位問題
項目架構先來張系統架構圖吧,這張圖來源自網絡,相信現在大部分中小企業的分布式集群架構都是類似這樣的設計:
簡要說明下分層架構:
通常情況下會有專門一臺堡壘機做統一的代理轉發,客戶端(pc,移動端等)訪問由nginx統一暴露的入口
nginx反向代理,負載均衡到web服務器,由tomcat組成的集群,web層僅僅是作為接口請求的入口,沒有實際的業務邏輯
web層再用rpc遠程調用注冊到zookeeper的dubbo服務集群,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 BaseResultimplements 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 結果枚舉類 統一管理 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 結果枚舉類 統一管理 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); } } }驗證
以上web層接口UserController繼承BaseController,統一捕獲異常
服務層假設拋出自定義系統異常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. 執行成功 返回結果 }
該接口聲明了異常事物回滾,發送異常時會全部回滾
步驟1數據入庫失敗,理論上是拿不到主鍵id的,此時應當拋出自定義異常,提示操作失敗
如果步驟1數據入庫成功,步驟2中數據入庫失敗,那么理論上步驟1中的數據應當也要回滾,如果此時強制返回異常結果,那么步驟1入庫數據則成為臟數據,此時拋出自定義異常是最合理的
最后的思考在實際問題場景中去閱讀源碼是最合適的,帶著問題有目的的看指定源碼會讓人有豁然開朗的感覺.
更多原創文章會第一時間推送公眾號【張少林同學】,歡迎關注!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/72830.html
摘要:前言記錄前后端分離的系統應用下應用場景用戶信息傳遞需求緣起照例先看看系統的一張經典架構圖,這張圖參考自網絡在自定義異常,你是怎么處理的中已經對該架構做了簡單說明,這里不再描述。 showImg(https://segmentfault.com/img/remote/1460000017839927?w=1024&h=768); 前言 記錄前后端分離的系統應用下應用場景————用戶信息傳...
摘要:全局變量局部變量全局函數一段也是一塊域。此時打印的自然是,要記住相當于,所以這時候改變的是局部變量,并沒有影響到全局變量,所以第二次打印的依然是。 在熟悉了瀏覽器的工作原理之后,今天我們來講講瀏覽器在從服務器獲取到網頁文件之后是如何解析的。了解了這個基礎知識,對敲出來的代碼,質量會有不小的提升。 一、瀏覽器如何解析html html文件在沒有寫入html標簽之前和txt文本是一個性質的...
摘要:全局變量局部變量全局函數一段也是一塊域。此時打印的自然是,要記住相當于,所以這時候改變的是局部變量,并沒有影響到全局變量,所以第二次打印的依然是。 在熟悉了瀏覽器的工作原理之后,今天我們來講講瀏覽器在從服務器獲取到網頁文件之后是如何解析的。了解了這個基礎知識,對敲出來的代碼,質量會有不小的提升。 一、瀏覽器如何解析html html文件在沒有寫入html標簽之前和txt文本是一個性質的...
摘要:全局變量局部變量全局函數一段也是一塊域。此時打印的自然是,要記住相當于,所以這時候改變的是局部變量,并沒有影響到全局變量,所以第二次打印的依然是。 在熟悉了瀏覽器的工作原理之后,今天我們來講講瀏覽器在從服務器獲取到網頁文件之后是如何解析的。了解了這個基礎知識,對敲出來的代碼,質量會有不小的提升。 一、瀏覽器如何解析html html文件在沒有寫入html標簽之前和txt文本是一個性質的...
閱讀 2128·2021-09-27 14:04
閱讀 1873·2019-08-30 15:55
閱讀 1698·2019-08-30 13:13
閱讀 1065·2019-08-30 13:07
閱讀 2742·2019-08-29 15:20
閱讀 3240·2019-08-29 12:42
閱讀 3324·2019-08-28 17:58
閱讀 3593·2019-08-28 17:56