Spring Boot統一異常處理最佳實踐

Fundebug發表於2019-01-16

摘要: SpringBoot異常處理。

Fundebug經授權轉載,版權歸原作者所有。

前言

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 中的 flagmessage 這種字串來當鍵很容易處理, 例如你這裡叫 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 表示操作成功, 12 等正數表示使用者輸入錯誤, -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: "DELETE",
        url: "/goods/delete",
        dataType: "json",
        success: function(result){
            handlerResult(result, deleteDone);
        }
    });
}

function deleteDone(data) {
    alert("刪除成功");
}
複製程式碼

showErrorhandlerResult 是公共方法, 分別用來顯示錯誤和統一處理結果集.

然後將主要精力放在傳送請求和處理正確結果的方法上即可, 如這裡的 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 來處理.

一個簡單的演示專案: github.com/zhaojun1998…

本文作者: 趙俊 本文連結: www.zhaojun.im/springboot-… 版權宣告: 本部落格所有文章除特別宣告外,均採用BY-NC-SA許可協議。轉載請註明出處!

相關文章