異常分類的最佳實踐?構建合理的異常體系

2024年2月6日 23点热度 0人点赞

前言

在上一篇文章中,我們闡述了如何優雅地處理異常,從創建、拋出到處理異常的完整鏈路中提供一種處理方案。在本文中,將嘗試以實際開發經驗對如何分類異常,構建一個合理的異常體系進行最佳實踐。

為什麼要自定義異常

在探究異常如何分類時,先思考下為什麼要自定義異常類?我認為有以下幾點原因

  1. 原始異常提供的信息載體較少。通常僅有Message、Cause可用,自定義用於擴展信息載體,如增加異常碼;
  2. 原始異常對用戶不友好。通過定義一個業務異常類來說明業務異常,並攜帶用戶友好的提示引導用戶執行正確的操作;
  3. 對不同的異常進行針對性的操作。像一些用於提示用戶的異常,如密碼錯誤、權限不足等應返回給用戶查看,且該類異常並不需要進行額外處理。而對於系統出現的底層異常,如數據庫語句執行失敗,該類異常需要打印詳細的現場日志信息,和對異常攜帶的信息轉換後返回給用戶。

系統異常很多開發者沒有重視,經常會看到界面錯誤彈框攜帶了系統底層信息,如接口參數解析異常

這種情況就會泄露系統底層信息,在信息安全方面,這是一個中危漏洞,歸根到底就是沒對系統異常進行轉換,隻是簡單地在異常處理器中把原始異常信息返回了。

總得來說,自定義異常對象是為了更好的對異常情況進行針對性的處理,同時提高代碼的可讀性。

異常如何分類

在系統中,我們通常根據業務場景會細分出認證類、業務類、系統類等異常。但本人認為不管異常怎麼細分,都能夠歸屬到兩類異常,一是讓用戶看的,即業務異常;二是讓開發人員看到,即系統異常。

業務異常

編寫業務代碼時,當業務邏輯未按照預定執行時,系統應拋出一個對用戶友好的異常給用戶,引導用戶執行正確的操作,最基礎的場景就是登陸時密碼錯誤:

if(用戶密碼不匹配){
    throw new BusinessException("密碼錯誤,請重新輸入");
}

這類可預期的,用於創建對用戶友好提示且能自行處理的異常,我將其稱為業務異常。該類異常由統一異常處理器處理後,應將攜帶的異常信息返回給用戶,因為這是業務信息。

系統異常

對於各種用戶無法處理的異常,通常是系統錯誤的運行(BUG),該類異常通常需要開發人員解決。出現該類異常後,應打印詳細的異常信息(現場信息),並在必要時進行報警,及時通知開發人員處理,同時攜帶的異常信息應轉換成用戶友好的提示,如“系統出錯了,請稍後再試”,避免向外界透露系統底層信息。

try {
    //業務代碼
}catch (Exception e){
    //必要的錯誤日志
    log.error("必要的錯誤日志", e);
    //異常轉換
    throw new SysException("系統錯誤");
}

結合TemplateException異常對象

在上文中,我們聲明了TemplateException異常對象,這裡我們結合該對象進行異常處理。

  1. 聲明業務異常和系統異常
/**
 * 該類表示一個業務異常
 * 業務異常通常是用戶可解決的錯誤,通過該類攜帶一個對用戶友好的提示
 */
public class BusinessException extends TemplateException{
    //define
}
/**
 * 該類表示一個系統錯誤
 * 系統錯誤通常是用戶無法解決異常,通過該類攜帶必要的現場信息
 */
public class SystemException extends TemplateException{
    //define
}

  1. 執行業務,聲明兩個接口
@RequestMapping("/businessException/{flag}")
public String businessException(@PathVariable String flag) {
    //測試業務異常
    String errorMessage = "flag輸入錯誤,當前參數為:"   flag;
    throw new BusinessException(errorMessage);
}
@RequestMapping("/systemException/{flag}")
public String systemException(@PathVariable String flag) {
    //測試系統異常
    try {
        //創建異常
        throw new NullPointerException();
    } catch (Exception e) {
        //攜帶詳細的現場信息
        throw new SystemException("出現無法解決的異常", e)
                .describe("flag: %s", flag)
                .describe("其他描述信息:%s", "其他描述信息");
    }
}

  1. 異常統一處理
@ExceptionHandler(BusinessException.class)
public String handleBusinessException(BusinessException exception) {
    String exceptionSubject = exception.getSubject();
    log.warn("業務異常:{}", exceptionSubject);
    //響應結果,直接返回業務異常攜帶的主題
    return exceptionSubject;
}
@ExceptionHandler(SystemException.class)
public String handleSystemException(SystemException exception) {
    List<String> descriptions = exception.getDescriptions();
    Throwable cause = exception.getCause();
    cause = Objects.isNull(cause) ? exception : cause;
    String exceptionSubject = exception.getSubject();
    log.error("系統異常:{}", exceptionSubject);
    for (String description : descriptions) {
        log.error("{}", description);
    }
    log.error("觸發異常對象", cause);
    //響應結果,對異常攜帶的主題進行轉換
    return "系統異常";
}

  1. 效果
2024-01-24 22:35:49.390  WARN 19260 --- [nio-8888-exec-1] c.x.m.e.handle.GlobalExceptionHandler    : 業務異常:flag輸入錯誤,當前參數為:1
2024-01-24 22:35:57.729 ERROR 19260 --- [nio-8888-exec-5] c.x.m.e.handle.GlobalExceptionHandler    : 系統異常:出現無法解決的異常
2024-01-24 22:35:57.729 ERROR 19260 --- [nio-8888-exec-5] c.x.m.e.handle.GlobalExceptionHandler    : flag: 2
2024-01-24 22:35:57.729 ERROR 19260 --- [nio-8888-exec-5] c.x.m.e.handle.GlobalExceptionHandler    : 其他描述信息:其他描述信息
2024-01-24 22:35:57.731 ERROR 19260 --- [nio-8888-exec-5] c.x.m.e.handle.GlobalExceptionHandler    : 觸發異常對象
java.lang.NullPointerException: null
	at com.xiaoyan.monadicapptemplate.controller.CommonController.systemException(CommonController.java:30) ~[classes/:na]

總結

本文中,根據異常情況的特點,將異常歸納為業務異常和系統異常,並結合TemplateException異常對象進行異常處理。系統可以基於這兩類對象延伸出細分類,但還是需要根據異常特點進行針對性的處理,即讓用戶看的,顯示異常細節;讓開發人員看的,則隱藏異常細節。

作者:故事還長_
鏈接:
https://juejin.cn/post/7327725079676633139