異常處理是 Java 健壯性的基石。本文從JVM 方法調用棧機制出發,系統講解異常傳播尋找 catch 塊、業務異常分層設計、異常轉譯與異常鏈、避免異常、異常恢復、catch 塊處理決策六大核心主題,構建完整異常處理知識體系。
main(),新線程棧底為 run();catch 塊 (按字節碼偏移量範圍、異常類型繼承關係);catch 塊執行;Throwable.printStackTrace() 輸出棧跡信息;
Exception (受檢異常)
├── BusinessException (業務異常基類)
│ ├── UserNotFoundException
│ ├── InsufficientBalanceException
│ └── DuplicateResourceException
├── ValidationException (參數校驗異常)
└── ExternalServiceException (外部依賴異常)
RuntimeException (非受檢異常)
├── SystemException (系統級錯誤:DB掛了、OOM、網絡不可達)
└── IllegalStateException / IllegalArgumentException (編程錯誤)
| 層級 | 典型異常 | 處理策略 |
|---|---|---|
| 用戶系統 | PwdOrUnameError, UserAlreadyExist | 返回友好錯誤碼/訊息給前端 |
| 賬戶系統 | AmountNotEnough, AccountFrozen | 事務回滾、記錄審計日志、告警 |
| 基礎設施 | DataSourceExhausted, RedisConnectionFailed | 降級、熔斷、重試、運維告警 |
異常類型應與拋出它的組件處於同一抽象層。
反例:DAO 拋 SQLException 直接透傳到 Controller → Controller 依賴數據庫細節。
正例:DAO 捕獲 SQLException → 包裝為 DataAccessException (或自定義 RepositoryException) → 拋給 Service。
try {
// 數據庫操作
} catch (SQLException e) {
// 關鍵:將原始異常作為 cause 傳入構造器
throw new RepositoryException("用戶查詢失敗", e);
}
日志輸出將同時包含 RepositoryException 與根因 SQLException,便於排查。
汽車拋錨 (車輛故障異常) → 員工遲到 (業務異常)。
員工不應拋「汽車故障」異常 (那不是發生在員工身上的事),
而應轉譯為「遲到」異常,並附帶原因「汽車故障」。
| 策略 | 說明 | 代碼示例 |
|---|---|---|
| 修正編程錯誤 | 空指針、越界、類轉換異常屬於 Bug,通過測試/靜態分析消除 | Objects.requireNonNull(arg, "arg must not be null") |
| 狀態檢查方法 | 提供 canXxx()/isXxxReady(),調用前預判 | if (pressureCooker.canOpen()) cooker.open(); |
| Optional 替代 null | 明確表達「可能缺失」語義,避免 NPE | Optional |
| 參數校驗前置 | 入口處統一校驗 (JSR-303 @Valid),異常早暴露 | @Valid @RequestBody UserDTO dto |
@Transactional 自動回滾;分布式事務 TCC/Saga 模式編寫補償邏輯。Collections.unmodifiableList、CopyOnWriteArrayList 思想)。
try {
riskyOperation();
} catch (SpecificException e) {
// 必須採取以下一種行動:
// 1. 處理並恢復:log.warn + 重試/降級/默認值
// 2. 重新拋出:throw e; (保留堆棧)
// 3. 異常轉譯:throw new BusinessException("業務含義", e);
// 4. 不捕獲:在方法簽名 throws,由上層決策
}
// 禁忌:catch 後只打印堆棧、吞掉異常、打印後繼續執行 (狀態不可知)
log.error("業務描述", e) 保留完整堆棧,避免 e.getMessage() 丟失上下文。異常處理不是「寫 try-catch」,而是系統穩定性、可維護性、可診斷性的架構級設計。