摘要: SpringBoot異常處理。
- 原文:Spring MVC/Boot 統一異常處理最佳實踐
- 作者:趙俊
前言
在 Web
開發中, 我們經常會需要處理各種異常, 這是一件棘手的事情, 對於很多人來說, 可能對異常處理有以下幾個問題:
- 什么時候需要捕獲(
try-catch
)異常, 什么時候需要拋出(throws
)異常到上層. - 在
dao
層捕獲還是在service
捕獲, 還是在controller
層捕獲. - 拋出異常后要怎么處理. 怎么返回給頁面錯誤信息.
異常處理反例
既然談到異常, 我們先來說一下異常處理的反例, 也是很多人容易犯的錯誤, 這里我們同時講到前端處理和后端處理 :
捕獲異常后只輸出到控制台
前端代碼
$.ajax({
type: "GET",
url: "/user/add",
dataType: "json",
success: function(data){
alert("添加成功");
}
});
后端代碼
try {
// do something
} catch (Exception e) {
e.printStackTrace();
}
這是見過最多的異常處理方式了, 如果這是一個添加商品的方法, 前台通過 ajax 發送請求到后端, 期望返回 json 信息表示添加結果. 但如果這段代碼出現了異常:
-
那么用戶看到的場景就是點擊了添加按鈕, 但沒有任何反應(其實是返回了 500 錯誤頁面, 但這里前端沒有監聽 error 事件, 只監聽了 success 事件. 但即使加上了
error: function(data) {alert("添加失敗");}
) 又如何呢? 到底因為啥失敗了呢, 用戶也不得而知. -
后台
e.printStackTrace()
打印在控制台的日志也會在漫漫的日志中被埋沒, 很可能會看不到輸出的異常. 但這並不是最糟的情況, 更糟糕的事情是連e.printStackTrace()
都沒有,catch
塊中是空的, 這樣后端的控制台中更是什么都看不到了, 這段代碼會像一個隱形的炸彈一樣一直埋伏在系統中.
混亂的返回方式
前端代碼
$.ajax({
type: "GET",
url: "/goods/add",
dataType: "json",
success: function(data) {
if (data.flag) {
alert("添加成功");
} else {
alert(data.message);
}
},
error: function(data){
alert("添加失敗");
}
});
后端代碼
@RequestMapping("/goods/add")
@ResponseBody
public Map add(Goods goods) {
Map map = new HashMap();
try {
// do something
map.put(flag, true);
} catch (Exception e) {
e.printStackTrace();
map.put("flag", false);
map.put("message", e.getMessage());
}
reutrn map;
}
這種方式捕獲異常后, 返回了錯誤信息, 且前台做了一定的處理, 看起來很完善? 但用 HashMap
中的 flag
和 message
這種字符串來當鍵很容易處理, 例如你這里叫 message
, 別人起名叫 msg
, 甚至有時手抖打錯了, 怎么辦? 前台再改成 msg
或其他的字符?, 前端后端這樣一直來回改?
更有甚者在情況 A 的情況下, 返回 json, 在情況 B 的情況下, 重定向到某個頁面, 這就更亂了. 對於這種不統一的結構處理起來非常麻煩.
異常處理規范
既然要進行統一異常處理, 那么肯定要有一個規范, 不能亂來. 這個規范包含前端和后端.
不要捕獲任何異常
對的, 不要在業務代碼中進行捕獲異常, 即 dao、service、controller 層的所以異常都全部拋出到上層. 這樣不會導致業務代碼中的一堆 try-catch
會混亂業務代碼.
統一返回結果集
不要使用 Map 來返回結果, Map 不易控制且容易犯錯, 應該定義一個 Java 實體類. 來表示統一結果來返回, 如定義實體類:
public class ResultBean<T> {
private int code;
private String message;
private Collection<T> data;
private ResultBean() {
}
public static ResultBean error(int code, String message) {
ResultBean resultBean = new ResultBean();
resultBean.setCode(code);
resultBean.setMessage(message);
return resultBean;
}
public static ResultBean success() {
ResultBean resultBean = new ResultBean();
resultBean.setCode(0);
resultBean.setMessage("success");
return resultBean;
}
public static <V> ResultBean<V> success(Collection<V> data) {
ResultBean resultBean = new ResultBean();
resultBean.setCode(0);
resultBean.setMessage("success");
resultBean.setData(data);
return resultBean;
}
// getter / setter 略
}
- 正常情況: 調用
ResultBean.success()
或ResultBean.success(Collection<V> data)
, 不需要返回數據, 即調用前者, 需要返回數據, 調用后者. 如:
@RequestMapping("/goods/add")
@ResponseBody
public ResultBean<Goods> getAllGoods() {
List<Goods> goods = goodsService.findAll();
return ResultBean.success(goods);
}
@RequestMapping("/goods/update")
@ResponseBody
public ResultBean updateGoods(Goods goods) {
goodsService.update(goods);
return ResultBean.success();
}
一般只有查詢方法需要調用 ResultBean.success(Collection<V> data)
來返回 N 條數據, 其他諸如刪除, 修改等方法都應該調用 ResultBean.success()
, 即在業務代碼中只處理正確的功能, 不對異常做任何判斷. 也不需要對 update 或 delete 的更新條數做判斷(個人建議, 實際需要根據業務). 只要沒有拋出異常, 我們就認為用戶操作成功了. 且操作成功的提示信息在前端處理, 不要后台返回 “操作成功” 等字段.
前台接受到的信息為:
{
"code": 0,
"message": "success",
"data": [
{
"name": "商品1",
"price": 50.00,
},
{
"name": "商品2",
"price": 99.99,
}
]
}
- 拋出異常: 拋出異常后, 我們應該調用
ResultBean.error(int code, String message)
, 來將狀態碼和錯誤信息返回, 我們約定code
為 0 表示操作成功,1
或2
等正數表示用戶輸入錯誤,-1
,-2
等負數表示系統錯誤.
前台接受到的信息為:
{
"code": -1,
"message": "XXX 參數有問題, 請重新填寫",
"data": null
}
前端統一處理:
返回的結果集規范后, 前端就很好處理了:
/**
* 顯示錯誤信息
* @param result: 錯誤信息
*/
function showError(s) {
alert(s);
}
/**
* 處理 ajax 請求結果
* @param result: ajax 返回的結果
* @param fn: 成功的處理函數 ( 傳入data: fn(result.data) )
*/
function handlerResult(result, fn) {
// 成功執行操作,失敗提示原因
if (result.code == 0) {
fn(result.data);
}
// 用戶操作異常, 這里可以對 1 或 2 等錯誤碼進行單獨處理, 也可以 result.code > 0 來粗粒度的處理, 根據業務而定.
else if (result.code == 1) {
showError(result.message);
}
// 系統異常, 這里可以對 -1 或 -2 等錯誤碼進行單獨處理, 也可以 result.code > 0 來粗粒度的處理, 根據業務而定.
else if (result.code == -1) {
showError(result.message);
}
// 如果進行細粒度的狀態碼判斷, 那么就應該重點注意這里沒出現過的狀態碼. 這個判斷僅建議在開發階段保留用來發現未定義的狀態碼.
else {
showError("出現未定義的狀態碼:" + result.code);
}
}
/**
* 根據 id 刪除商品
*/
function deleteGoods(id) {
$.ajax({
type: "GET",
url: "/goods/delete",
dataType: "json",
success: function(result){
handlerResult(result, deleteDone);
}
});
}
function deleteDone(data) {
alert("刪除成功");
}
showError
和 handlerResult
是公共方法, 分別用來顯示錯誤和統一處理結果集.
然后將主要精力放在發送請求和處理正確結果的方法上即可, 如這里的 deleteDone 函數, 用來處理操作成功給用戶的提示信息, 正所謂各司其職, 前端負責操作成功的消息提示更合理, 而錯誤信息只有后台知道, 所以需要后台來返回.
后端統一處理異常
說了這么多, 還沒講到后端不在業務層捕獲任何異常的事, 既然所有業務層都沒有捕獲異常, 那么所有的異常都會拋出到 Controller 層, 我們只需要用 AOP 對 Controller 層的所有方法處理即可.
好在 Spring 為我們提供了一個注解, 用來統一處理異常:
@ControllerAdvice
@ResponseBody
public class WebExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(WebExceptionHandler.class);
@ExceptionHandler
public ResultBean unknownAccount(UnknownAccountException e) {
log.error("賬號不存在", e);
return ResultBean.error(1, "賬號不存在");
}
@ExceptionHandler
public ResultBean incorrectCredentials(IncorrectCredentialsException e) {
log.error("密碼錯誤", e);
return ResultBean.error(-2, "密碼錯誤");
}
@ExceptionHandler
public ResultBean unknownException(Exception e) {
log.error("發生了未知異常", e);
// 發送郵件通知技術人員.
return ResultBean.error(-99, "系統出現錯誤, 請聯系網站管理員!");
}
}
在這里統一配置需要處理的異常, 同樣, 對於未知的異常, 一定要及時發現, 並進行處理. 推薦出現未知異常后發送郵件, 提示技術人員.
總結
總結一下統一異常處理的方法:
- 不使用隨意返回各種數據類型, 要統一返回值規范.
- 不在業務代碼中捕獲任何異常, 全部交由
@ControllerAdvice
來處理.
一個簡單的演示項目: https://github.com/zhaojun1998/exception-handler-demo
本文作者: 趙俊
本文鏈接: http://www.zhaojun.im/springboot-exception/
版權聲明: 本博客所有文章除特別聲明外,均采用BY-NC-SA許可協議。轉載請注明出處!