自定義異常是領域模型表達業務錯誤語義的關鍵手段。本文涵蓋設計原則、四步實現、異常鏈保留、序列化考量、常見反模式與單元測試策略。
InsufficientBalanceException 優於 BusinessException)。RuntimeException(非受檢,框架統一攔截);可恢復的系統異常繼承 Exception(受檢,強制處理)。final,構造器注入,無 setter。public class InsufficientBalanceException extends RuntimeException {
private final String accountId;
private final BigDecimal required;
private final BigDecimal available;
private final String errorCode = "ACCT_001";
public InsufficientBalanceException(String accountId, BigDecimal required, BigDecimal available) {
super(String.format("賬戶 %s 餘額不足:需 %.2f,可用 %.2f", accountId, required, available));
this.accountId = accountId;
this.required = required;
this.available = available;
}
// 異常鏈構造器(保留根因)
public InsufficientBalanceException(String accountId, BigDecimal required, BigDecimal available, Throwable cause) {
this(accountId, required, available);
initCause(cause);
}
// Getters 供上層處理器提取結構化信息
public String getAccountId() { return accountId; }
public BigDecimal getRequired() { return required; }
public BigDecimal getAvailable() { return available; }
public String getErrorCode() { return errorCode; }
}
// Service 層拋出
public void transfer(String from, String to, BigDecimal amount) {
Account fromAcc = accountRepo.find(from);
if (fromAcc.getBalance().compareTo(amount) < 0) {
throw new InsufficientBalanceException(from, amount, fromAcc.getBalance());
}
// ...
}
// Controller/全局異常處理器捕獲
@ExceptionHandler(InsufficientBalanceException.class)
public Result handle(InsufficientBalanceException e) {
log.warn("轉賬失敗: {}", e.getMessage(), e); // 保留堆棧
return Result.fail(e.getErrorCode(), e.getMessage());
}
| 反模式 | 危害 | 正確做法 |
|---|---|---|
捕獲後 throw new RuntimeException(e.getMessage()) | 丟失堆棧、異常類型、原因鏈 | throw new BizException("msg", e) 保留 cause |
| 用異常控制業務流程(如「查無數據」拋異常) | 性能差、可讀性低 | 返回 Optional/Result 或空集合 |
定義過於通用的 BaseException 並捕獲它 | 掩蓋具體錯誤,難以差異化處理 | 針對性捕獲具體異常類 |
異常類無 serialVersionUID 或非 final 字段 | 分布式序列化失效/狀態不一致 | 實現 Serializable,字段 final |
靜態常量異常實例(如 static final MyException EX = new MyException()) | 堆棧固化在類加載時,誤導排查 | 每次 new 獲取當前堆棧 |
@Test
void transfer_insufficientBalance_throwsException() {
when(repo.find("A")).thenReturn(new Account("A", BigDecimal.TEN));
assertThatThrownBy(() -> service.transfer("A", "B", BigDecimal.valueOf(20)))
.isInstanceOf(InsufficientBalanceException.class)
.extracting("accountId", "required", "available")
.containsExactly("A", BigDecimal.valueOf(20), BigDecimal.TEN);
}
Serializable,聲明 private static final long serialVersionUID = 1L;。Connection、ThreadLocal)。