
前言
記錄Dubbo
對於自定義異常的處理方式.
實現目標
- 服務層異常,直接向上層拋出,
web
層統一捕獲處理 - 如果是系統自定義異常,則返回
{"code":xxx,"msg":yyy}
其中code
對應為錯誤碼
,msg
對應為異常信息 - 如果非系統自定義異常,返回
{"code":-1,"msg":"未知錯誤"}
,同時將異常堆棧信息輸出到日志,便於定位問題
項目架構
先來張系統架構圖吧,這張圖來源自網絡,相信現在大部分中小企業的分布式集群架構都是類似這樣的設計:

簡要說明下分層架構:
- 通常情況下會有專門一台
堡壘機
做統一的代理轉發,客戶端(pc,移動端等)訪問由nginx
統一暴露的入口 nginx
反向代理,負載均衡到web
服務器,由tomcat
組成的集群,web
層僅僅是作為接口請求的入口,沒有實際的業務邏輯web
層再用rpc
遠程調用注冊到zookeeper
的dubbo
服務集群,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<T> implements 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}
驗證
-
以上
web
層接口UserController
繼承BaseController
,統一捕獲異常 -
服務層假設拋出自定義系統異常
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入庫數據則成為臟數據,此時拋出自定義異常是最合理的
最后的思考
在實際問題場景中去閱讀源碼是最合適的,帶着問題有目的的看指定源碼會讓人有豁然開朗的感覺.
更多原創文章會第一時間推送公眾號【張少林同學】,歡迎關注!
