一、背景
1、系統在運行的時候可能會有下面這些種類的錯誤/失敗發生:
(1) 依賴組件掛了,可能是 db,可能是 mq,可能是 cache。
(2)依賴服務掛了,可能是別人給你提供的 http/rpc 服務掛了。
(3)可能是你的依賴方超時了。
(4)可能是調用方的參數有問題。
(5)可能是調用方的參數無法正確地通過校驗。
(6)可能是用戶的某種操作在業務邏輯上不合理性,不能夠接着讓他執行下去
(7)還可能是程序自身出錯了,比如數組越界,把 null 當成了某種合法的數據結構等等。
上面這些情況都是有很大概率發生的,當這種情況發生的時候,如果用戶向你反饋了問題,你要怎么進行跟蹤呢?
2、互聯網公司在系統內部出了點什么問題的時候,展現給用戶的是什么?白屏、無任何響應、nginx 504 Gateway Timeout。用戶都不知道這些是什么。
二、目標
1、用戶角度
對於產品的用戶來講,希望的是無論任何情況下都要有一個明確的反饋,正常情況下自不用說。而特殊情況下,也應該看得到系統到底出了什么問題,用戶網絡不行了就告訴用戶是網絡問題,不要出現一堆莫名其妙的英文。在各種異常情況下,要保證用戶能夠恢復到正常使用中去。不要顯示用戶看不懂的任何信息。不要什么都不顯示(白屏)。用戶的想法會要求在服務端就有完善的錯誤兜底。而不是寫完正常的業務邏輯就完事了。
2、研發角度
實際上就是要有調用鏈的錯誤存儲邏輯,比如errors 應該是能夠一路把上游的錯誤串下來,而不是直接只存儲當前這一級出了什么問題。
錯誤碼最大的好處大概就是能夠按照錯誤碼建立自己的業務錯誤字典,這個字典你甚至可以在客戶端進行存儲,當用戶使用報錯的時候可以直接彈出錯誤原因自查選項以及恢復建議。錯誤碼對於用戶和客服,客服和技術人員之間溝通也有很大的好處,至少在軟件使用和技術方面上的溝通成本會下降很多。
當看到錯誤碼或者錯誤信息的時候,能馬上找到代碼的位置就事半功倍。
三、如何存儲
1、所有錯誤消息都已經確定,可以寫在程序里,用枚舉比用全局常量更容易。
2、錯誤消息不能固定,可能在應用過程中添加,保存在數據庫中。專門寫一套管理類來處理,用 HashTable 或者 HashMap 之類的方式來實現。
3、把錯誤的key放在java的枚舉里面,然后把錯誤的key和錯誤的信息的映射放在外部文件中,比如properties文件里。在運行時,根據錯誤枚舉的key來實時從文件中取出錯誤文本就可以了,因為錯誤不是經常發生的,實時讀取錯誤信息應該沒有問題,當然也可以在程序啟動時候把所有的錯誤信息讀進來然后放到緩存里以提高性能。
四、設計錯誤編碼的具體實現
1、統一設計目標
統一展示用戶提示信息:編號“-網絡異常,請聯系相關人員處理!”
例如:x1010001-網絡異常,請聯系相關人員處理!
2、如何存儲
在程序中使用枚舉對錯誤編碼進行存儲。
3、如何獲取
(1)系統bug級別的:直接在枚舉中查看錯誤信息
(2)非系統bug級別的:設計單獨的接口獲取錯誤信息
綜上所述:為了很好的擴展功能,建議設計單獨的接口獲取錯誤信息
4、具體編碼設計
第1位(固定,用x標識,沒有特殊設計含義,只是為了方便存儲)
第2位(錯誤級別,1為非系統bug,2為系統bug需要改代碼)
第3-4位(功能模塊)
第5-8位(錯誤編碼,從0001開始,依次順延)
實例:x1010001
五、開發規范(在代碼中使用“拋異常”還是“返回錯誤碼”)
1、對於公司外的 http/api 開放接口必須使用“錯誤碼”;而應用內部推薦異常拋出。
2、跨應用間 RPC 調用,優先考慮使用 Result 方式,封裝 isSuccess()方法、“錯誤碼”、“錯誤簡短信息”。
(1)關於 RPC 方法返回方式使用 Result 方式的理由
使用拋異常返回方式,調用方如果沒有捕獲到就會產生運行時錯誤。
如果不加棧信息,只是 new 自定義異常,加入自己的理解的 error message,對於調用端解決問題的幫助不會太多。
如果加了棧信息,在頻繁調用出錯的情況下,數據序列化和傳輸的性能損耗也是問題。
(2)具體實現
統一返回結果展示,定義Result,封裝 isSuccess()方法、“錯誤碼”、“錯誤信息”。並提供ResultUtil工具類方便使用。
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
private boolean success = false;//是否成功
private String code;//狀態碼
private String message;//信息
private T data;//裝載數據
public Result() {}
/**
* @param success 是否成功
*/
public Result(boolean success) {
this.success = success;
}
/**
* @param success 是否成功
* @param message 消息
*/
public Result(boolean success, String message) {
this.success = success;
this.message = message;
}
/**
* @param success 是否成功
* @param message 消息
* @param data 數據
*/
public Result(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
}
/**
* @param code 錯誤碼
* @param message 錯誤信息
*/
public Result(String code, String message) {
this.code = code;
this.message = message;
}
public boolean isSuccess() {
return success;
}
//省略get和set
}
public class ResultUtil {
/**
* return success
* @param data
* @return
*/
public static <T> Result<T> success(T data) {
Result<T> result = new Result<T>();
result.setCode("0");
result.setData(data);
result.setSuccess(true);
result.setMessage("success");
return result;
}
/**
* return success
* @return
*/
public static Result success() {
return success(null);
}
/**
* return error
*
* @param code 錯誤碼
* @param msg 錯誤信息
* @return
*/
public static Result error(String code, String msg) {
Result result = new Result();
result.setCode(code);
result.setMessage(msg);
return result;
}
/**
* 枚舉
*
* @param status
* @return
*/
public static Result error(ErrorCodeEnum status) {
return error(status.getCode(), status.getMsg());
}
}
3、統一異常處理
(1)自定義異常類(運行時異常),並使用全局異常處理類GlobalErrorHandler統一處理所有異常。
若為json,直接返回Result,若為html,返回error頁面,並顯示錯誤信息。
(2)具體實現
public class CommonException extends RuntimeException{
private static final long serialVersionUID = 1L;
private String code;
private String msg;
public CommonException(String code,String msg){
this.code = code;
this.msg = msg;
}
public CommonException(ErrorCodeEnum resultEnum){
this.code = resultEnum.getCode();
this.msg = resultEnum.getMsg();
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
@ControllerAdvice
public class GlobalErrorHandler {
private final static String DEFAULT_ERROR_VIEW = "error";//錯誤信息頁
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Object handleException(Exception e,HttpServletRequest request) {
Result obj = new Result();
if (e instanceof CommonException) {
CommonException commone = (CommonException)e;
obj = ResultUtil.error(commone.getCode(), commone.getMsg());
}else{
obj = ResultUtil.error(ErrorCodeEnum.x9999999);
}
//使用HttpServletRequest中的header檢測請求是否為ajax, 如果是ajax則返回json, 如果為非ajax則返回view(即ModelAndView)
String contentTypeHeader = request.getHeader("Content-Type");
String acceptHeader = request.getHeader("Accept");
String xRequestedWith = request.getHeader("X-Requested-With");
if ((contentTypeHeader != null && contentTypeHeader.contains("application/json"))
|| (acceptHeader != null && acceptHeader.contains("application/json"))
|| "XMLHttpRequest".equalsIgnoreCase(xRequestedWith)) {
return obj;
} else {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("code", obj.getCode());
modelAndView.addObject("msg", obj.getMessage());
modelAndView.addObject("url", request.getRequestURL());
modelAndView.setViewName(DEFAULT_ERROR_VIEW);
return modelAndView;
}
}
}