異常繼承體系圖
java.lang.Throwable
├── java.lang.Error // 嚴重錯誤,不可恢復 (OOM, StackOverflow)
└── java.lang.Exception // 受檢異常基類
├── java.lang.RuntimeException // 非受檢異常基類 (NPE, IndexOutOfBounds)
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ └── ...
├── java.io.IOException // 典型受檢異常
├── java.sql.SQLException
├── java.text.ParseException
└── ... (其他受檢異常)
三大類別核心差異
| 維度 | Error | Exception (受檢) | RuntimeException (非受檢) |
| 繼承自 | Throwable | Exception | Exception |
| 編譯器強制處理 | 否 | 是 (try-catch 或 throws) | 否 |
| 典型場景 | JVM 級故障、資源耗盡 | 外部資源失敗 (IO、DB、網絡) | 編程邏輯錯誤、參數非法 |
| 是否應捕獲處理 | 絕不 | 必須 | 可選 (通常由全局處理器捕獲) |
| 代表類 | OutOfMemoryError、StackOverflowError、NoClassDefFoundError | IOException、SQLException、ParseException、ClassNotFoundException | NullPointerException、ArrayIndexOutOfBoundsException、IllegalStateException |
處理策略決策樹
發生異常
│
├─ 是 Error? ──是──► 不捕獲,讓 JVM 崩潰/重啟,排查根因 (內存洩漏、死循環、類加載失敗)
│
└─ 否 (是 Exception)
│
├─ 是 RuntimeException? ──是──►
│ ├─ 能預防? ──是──► 修正代碼邏輯 (空指針檢查、邊界判斷)
│ └─ 不能預防? ──► 全局異常處理器記錄日誌、返回友好錯誤頁/JSON
│
└─ 否 (受檢異常)
│
├─ 當前層能有意義處理? ──是──► try-catch 處理 (重試、降級、轉換錯誤碼)
│
└─ 否 ──►
├─ 包裝為 RuntimeException 拋出 (保留原異常作為 cause)
└─ 或在方法簽名 throws 聲明 (API 契約要求)
代碼示範:標準處理模式
1. 受檢異常 → 業務異常轉換 (推薦)
@Repository
public class UserDao {
public User findById(Long id) {
try {
return jdbcTemplate.queryForObject("SELECT * FROM user WHERE id=?",
(rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name")), id);
} catch (EmptyResultDataAccessException e) {
throw new BusinessException("USER_NOT_FOUND", "用戶不存在: " + id, e);
} catch (DataAccessException e) {
throw new SystemException("DB_ERROR", "數據庫訪問失敗", e);
}
}
}
2. 運行時異常 → 全局統一處理 (Spring Boot)
3. 防禦性編程:參數校驗拋出運行時異常
public void transfer(Account from, Account to, BigDecimal amount) {
// 非空檢查
Objects.requireNonNull(from, "轉出賬戶不能為空");
Objects.requireNonNull(to, "轉入賬戶不能為空");
Objects.requireNonNull(amount, "金額不能為空");
// 業務規則校驗
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("轉賬金額必須大於零");
}
if (from.getBalance().compareTo(amount) < 0) {
throw new InsufficientBalanceException("餘額不足: " + from.getBalance());
}
// 核心邏輯...
}
常見反模式與修正
| 反模式 | 問題 | 修正 |
catch (Exception e) { e.printStackTrace(); } | 吞噬異常,調用方不知失敗 | 至少重新拋出或包裝拋出 |
throws Exception 在方法簽名 | 調用方無法針對性處理 | 聲明具體異常或包裝為運行時異常 |
| 捕獲 Error 類 | 掩蓋嚴重故障 | 絕不捕獲 Error |
| 用異常控制業務流程 | 性能差、可讀性差 | 用條件判斷、Optional、Result 模式 |
總結
- Error:JVM 級災難,不捕獲,靠監控告警
- 受檢異常:外部依賴不可控風險,要么當前層處理,要么包裝為運行時異常拋出
- 運行時異常:編程錯誤,優先預防,不可預防則全局統一處理
- 異常鏈
cause 必須保留,便於根因排查
- 業務代碼盡量無 try-catch,交由切面/全局處理器