統一異常處理到底應該怎麼做呢?

前言

在 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;
}

一大堆的try catch語句

我們在有的項目中會發現try catch的代碼很多,看起來很頭疼啊。

導致前後端嚴重耦合

使用異常處理的好處

  • 代碼更加乾淨清爽
  • 異常統一由項目經理leader使用AOP來進行處理和定義,不需要每個開發自己定義維護
  • 統一的接口返回值,前後端交互更簡單。
  • 充分解耦前後端,面向接口編程(符合契約式編程原則)

Java異常分類

在這裏插入圖片描述
  Java把異常當做對象來處理。Throwable是所有錯誤或異常的超類。Throwable類有兩個直接子類:Error類和Exception類。
1、Error是指java運行時系統的內部錯誤和資源耗盡錯誤,是程序無法處理的異常,應用程序不會拋出該類對象。
2、Exception是程序本身可以處理的異常,應儘可能去處理這些異常。Exception分兩類,一個是運行時異常RuntimeException,一個是檢查異常CheckedException。
3、CheckedException一般是外部錯誤,這種異常都發生在編譯階段,Java編譯器會強制程序去捕獲此類異常。
4、RuntimeException是那些可能在Java 虛擬機正常運行期間拋出的異常的超類。這種錯誤是由程序員引起的錯誤,可以修正代碼解決。

異常處理規範應用

不要捕獲任何異常

  不要在業務代碼中進行捕獲異常, 即 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 略
}

前端代碼

{
    "code": 0,
    "message": "success",
    "data": [
        {
            "name": "商品1",
            "price": 50.00,
        },
        {
            "name": "商品2",
            "price": 99.99,
        }
    ]
}

前端統一處理異常

/**
 * 顯示錯誤信息
 * @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("刪除成功");
}

後端統一處理異常

  用@ControllerAdvice+@ExceptionHandler實現全局異常處理

  通常在Controller層需要去捕獲service層的異常,防止返回一些不友好的錯誤信息到客戶端,但如果Controller層每個方法都用模塊化的try-catch代碼去捕獲異常,會很難看也難維護。
異常處理最好是解耦的,並且都放在一個地方集中管理。Spring能夠較好的處理這種問題,核心如下,這裏主要關注前兩個:

  • @ExceptionHandler:統一處理某一類異常,從而能夠減少代碼重複率和複雜度
  • @ControllerAdvice:異常集中處理,更好的使業務邏輯與異常處理剝離開
  • @ResponseStatus:可以將某種異常映射爲HTTP狀態碼

單使用@ExceptionHandler,只能在當前Controller中處理異常,與@ControllerAdvice組合使用,則可以實現全局異常處理,不用每個controller都配置。

@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, "系統出現錯誤, 請聯繫網站管理員!");
    }
}


小結

在項目中我們將異常處理從業務代碼中解耦出來,實現異常信息的統一處理和維護,減少了模板代碼,改善了代碼的風格。

感謝您的閱讀~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章