前言
記錄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 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 結果列舉類 統一管理 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入庫資料則成為髒資料,此時丟擲自定義異常是最合理的
最後的思考
在實際問題場景中去閱讀原始碼是最合適的,帶著問題有目的的看指定原始碼會讓人有豁然開朗的感覺.
更多原創文章會第一時間推送公眾號【張少林同學】,歡迎關注!