首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Java 自定義異常設計指南:從入門到最佳實踐

     編輯:Java 綜合教學

Java 自定義異常設計指南:從入門到最佳實踐

異常處理是 Java 程式健壯性的基石。雖然 JDK 提供了豐富的標準異常體系,但實際業務中往往需要表達特定業務錯誤語義。本文系統講解自定義異常的設計原則、實現模式與工程落地經驗。

一、為什麼需要自定義異常?

  1. 業務語義表達:標準異常 (如 NullPointerException) 無法表達「用戶餘額不足」、「訂單狀態非法」等業務概念
  2. 統一錯誤碼體系:前後端分離架構下,定義標準化錯誤碼 (如 BIZ_1001) 便於前端國際化提示與錯誤追蹤
  3. 敏感資訊隔離:封裝底層堆疊追蹤,避免資料庫結構、第三方介面細節洩露給客戶端
  4. 精準異常攔截:配合 Spring @ControllerAdvice 實現分類處理:業務異常返回友好提示、系統異常記錄日誌並告警

二、異常體系設計原則

1. 繼承層級選擇

  • 受檢異常 (extends Exception):呼叫者必須處理的業務預期錯誤 (如餘額不足、庫存不足)
  • 非受檢異常 (extends RuntimeException):程式邏輯錯誤、系統級故障、或不想強制呼叫者捕獲的業務拒絕 (如參數驗證失敗、權限不足)

建議:現代框架 (Spring、MyBatis) 傾向使用 RuntimeException 衍生類,減少樣板碼,配合全域異常處理器統一響應。

2. 核心欄位設計

public class BizException extends RuntimeException {
    private final String errorCode;      // 業務錯誤碼:BIZ_1001
    private final String userMessage;    // 使用者友好提示:「餘額不足,請充值」
    private final String devMessage;     // 開發者除錯訊息:包含參數、堆疊等
    private final Map context; // 擴充上下文:userId、orderId 等
    
    // 多種建構子支援:錯誤碼、訊息、原因異常、上下文靈活組合
}

三、標準實現範本

/**
 * 統一業務異常基類
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class BizException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    
    private final String errorCode;
    private final String userMessage;
    private final Map context = new HashMap<>();
    
    public BizException(String errorCode, String userMessage) {
        super(userMessage);
        this.errorCode = errorCode;
        this.userMessage = userMessage;
    }
    
    public BizException(String errorCode, String userMessage, Throwable cause) {
        super(userMessage, cause);
        this.errorCode = errorCode;
        this.userMessage = userMessage;
    }
    
    // 鏈式新增上下文
    public BizException addContext(String key, Object value) {
        context.put(key, value);
        return this;
    }
    
    // 常用錯誤碼常量介面
    public interface ErrorCodes {
        String VALIDATION_FAILED = "BIZ_1000";
        String INSUFFICIENT_BALANCE = "BIZ_1001";
        String ORDER_NOT_FOUND = "BIZ_2001";
        // ...
    }
}

四、實戰場景範例

場景 1:參數驗證失敗

@PostMapping("/transfer")
public Result transfer(@Valid @RequestBody TransferDTO dto) {
    if (dto.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
        throw new BizException(BizException.ErrorCodes.VALIDATION_FAILED, 
            "轉帳金額必須大於零").addContext("field", "amount");
    }
    // ...
}

場景 2:業務規則違反

public void deductBalance(Long userId, BigDecimal amount) {
    UserAccount account = accountDao.lockForUpdate(userId);
    if (account.getBalance().compareTo(amount) < 0) {
        throw new BizException(BizException.ErrorCodes.INSUFFICIENT_BALANCE, 
            "帳戶餘額不足")
            .addContext("userId", userId)
            .addContext("available", account.getBalance())
            .addContext("required", amount);
    }
    // ...
}

場景 3:整合第三方服務異常轉換

try {
    PaymentResponse resp = paymentClient.pay(req);
} catch (PaymentGatewayException e) {
    // 封裝第三方錯誤碼,不洩露內部細節
    throw new BizException("BIZ_3001", "支付服務暫時不可用,請稍後重試", e)
        .addContext("gatewayCode", e.getCode())
        .addContext("orderNo", req.getOrderNo());
}

五、全域異常處理器實現

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    @ExceptionHandler(BizException.class)
    public Result handleBizException(BizException e, HttpServletRequest request) {
        log.warn("業務異常: code={}, msg={}, path={}, context={}", 
            e.getErrorCode(), e.getUserMessage(), request.getRequestURI(), e.getContext());
        return Result.fail(e.getErrorCode(), e.getUserMessage());
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result handleValidation(MethodArgumentNotValidException e) {
        String msg = e.getBindingResult().getFieldErrors().stream()
            .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
            .collect(Collectors.joining("; "));
        return Result.fail(BizException.ErrorCodes.VALIDATION_FAILED, msg);
    }
    
    @ExceptionHandler(Exception.class)
    public Result handleSystemException(Exception e, HttpServletRequest request) {
        log.error("系統異常: path={}", request.getRequestURI(), e);
        return Result.fail("SYS_9999", "系統繁忙,請稍後重試");
    }
}

六、避坑指南與最佳實踐

反模式問題正確做法
大量 catch 後 log 後拋新 RuntimeException丟失原始堆疊,難以排查使用帶 cause 參數的建構子:new BizException(code, msg, cause)
異常類爆炸:每個錯誤一個 Class類數量膨脹,維護困難採用錯誤碼 + 參數化訊息,單一 BizException 覆蓋 90% 場景
在高頻路徑使用異常控制流程效能極差 (填充堆疊追蹤開銷大)改用 Result/Optional 模式,異常僅用於真正的錯誤場景
錯誤訊息硬編碼中文無法國際化,前端難以解析錯誤碼 + 訊息鍵 (i18n key),前端根據語言包顯示

七、總結

優秀的異常設計應具備:語義清晰 (錯誤碼體系)、資訊完整 (上下文、原因鏈)、處理統一 (全域攔截)、安全可控 (敏感資訊過濾)。採用「單一基類 + 錯誤碼枚舉 + 上下文擴充」模式,配合 Spring 全域異常處理,即可構建可維護、可擴充的企業級異常處理體系。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved