Java 異常機制將錯誤傳遞從「返回碼檢查」升級為「類型化、可傳播、可攔截」的結構化流程。本文系統覆蓋異常體系、處理語法、分類哲學、設計原則、反模式、架構級實踐,助你建立生產級異常治理體系。
java.lang.Throwable
├── java.lang.Error // JVM/系統級嚴重錯誤,通常不可恢復
│ ├── OutOfMemoryError
│ ├── StackOverflowError
│ ├── NoClassDefFoundError
│ └── ...
└── java.lang.Exception // 應用層可預期、可恢復問題
├── java.lang.RuntimeException // 非受檢異常:程式邏輯錯誤,編譯器不強制處理
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ ├── IndexOutOfBoundsException
│ └── ...
└── 受檢異常 // 編譯器強制 catch/throws
├── IOException
├── SQLException
├── ClassNotFoundException
├── InterruptedException
└── ...
| 維度 | 受檢異常 | 非受檢異常 | Error |
|---|---|---|---|
| 編譯器檢查 | 強制 catch 或 throws | 不強制 | 不強制 |
| 典型場景 | 外部資源失效 (IO、DB、網絡) | 程式 Bug (空指針、越界、參數錯) | JVM/系統崩潰 |
| 處理策略 | 重試、降級、轉換業務異常 | 修代碼、防御性編程、斷言 | 監控告警、重啟、容災 |
| API 設計 | 方法簽名聲明 `throws` | 文檔 `@throws`、不聲明 | 不捕獲、不聲明 |
try {
// 高風險代碼
String line = reader.readLine();
} catch (IOException e) {
// 處理具體異常
log.error("讀取失敗", e);
throw new BusinessException("READ_FAIL", e);
} catch (Exception e) {
// 兜底 (不推薦常用)
log.error("未知錯誤", e);
} finally {
// 無條件執行:資源釋放、狀態重置
// 即使 catch 中 return/throw 也會執行
// 除非 System.exit(0)、JVM 崩潰、無限循環
}
// 合併捕獲:同層級異常
catch (IOException | SQLException e) {
log.error("資源訪問異常", e);
}
// 精確捕獲:子類在前,父類在後
catch (FileNotFoundException e) { ... }
catch (IOException e) { ... } // 父類放後
// 實作 AutoCloseable 的資源自動關閉,即使異常也關閉
try (FileInputStream fis = new FileInputStream("a.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(fis))) {
return br.readLine();
} // 編譯器自動生成 finally { br.close(); fis.close(); }
// 自定義資源
public class DbConnection implements AutoCloseable {
public void close() { /* 釋放連接 */ }
}
try (DbConnection conn = pool.getConnection()) {
// 業務邏輯
}
// 保留原始堆疊:cause 參數
throw new BusinessException("DB_ERROR", sqlException);
// 自定義異常類
public class BusinessException extends RuntimeException {
private final String code;
public BusinessException(String code, Throwable cause) {
super(cause.getMessage(), cause); // 必須傳 cause
this.code = code;
}
}
❌ `catch (Exception e) { log }` — 覆蓋式處理,掩蓋真實問題。
✅ 針對 `IOException` 重試、`SQLException` 熔斷、`BusinessException` 返回錯誤碼。
跨層傳播時,將技術異常包裝為業務異常:
// DAO 層
try { jdbc.update(sql); }
catch (SQLException e) { throw new DataAccessException("UPDATE_FAIL", e); }
// Service 層
try { dao.update(); }
catch (DataAccessException e) { throw new BusinessException("ORDER_UPDATE_FAIL", e); }
// Controller 層全局處理器統一返回 JSON
// ❌ 用異常控制流程
try { map.get(key).doSomething(); } catch (NPE) { ... }
// ✅ 防御性檢查
if (map.containsKey(key)) map.get(key).doSomething();
// 或
map.getOrDefault(key, defaultObj).doSomething();
| 層 | 異常類型 | 職責 | 示例 |
|---|---|---|---|
| Controller / Gateway | 統一響應包裝 | HTTP 狀態碼、錯誤碼、用戶友好訊息 | `GlobalExceptionHandler` → `R.fail(400, "參數錯誤")` |
| Service (業務邏輯) | 業務異常 | 表達業務規則違反,攜帶錯誤碼/參數 | `InsufficientBalanceException(code="BAL_001", balance=100)` |
| Repository / DAO | 數據訪問異常 | 包裝技術異常,屏蔽實現細節 | `DataAccessException(cause=SQLException)` |
| Client / SDK | 客戶端異常 | 網絡、序列化、超時、熔斷 | `RemoteServiceException`, `CircuitBreakerOpenException` |
// 使用 Resilience4j / Spring Retry
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2))
@CircuitBreaker(name = "paymentService", fallbackMethod = "payFallback")
public PaymentResult pay(PaymentRequest req) { ... }
public PaymentResult payFallback(PaymentRequest req, Exception e) {
return PaymentResult.pending("支付系統繁忙,請稍後查詢結果");
}
| 反模式 | 危害 | 修正 |
|---|---|---|
| 吞沒異常 `catch {}` | Bug 隱形、難排查 | 至少記錄日誌、或拋出包裝異常 |
| 覆蓋式捕獲 `catch (Exception)` | 掩蓋具體異常、誤處理 | 精確捕獲、多 catch、兜底僅記錄 |
| 異常轉換丟失堆疊 `throw new Ex(e.getMessage())` | 斷裂調用鏈、定位困難 | `throw new Ex("msg", e)` 保留 cause |
| 用異常做流程控制 | 效能差、語義混淆 | 條件判斷、Optional、Result 模式 |
| 在 finally 中拋出異常 | 掩蓋 try/catch 中的原始異常 | finally 僅釋放資源,內部 try-catch |
| 棄用 checked 異常全包為 RuntimeException | 調用者感知不到必須處理的風險 | 保留 checked 異常或文檔化 @throws |
// 業務異常定義
public class InsufficientFundsException extends BusinessException {
public InsufficientFundsException(String accountId, BigDecimal balance, BigDecimal amount) {
super("INSUFFICIENT_FUNDS",
String.format("賬戶 %s 餘額 %s 不足支付 %s", accountId, balance, amount),
null);
}
}
// Service 層
@Transactional
public void transfer(String from, String to, BigDecimal amount) {
Account fromAcc = accountRepo.findById(from).orElseThrow(
() -> new BusinessException("ACC_NOT_FOUND", "轉出賬戶不存在"));
Account toAcc = accountRepo.findById(to).orElseThrow(
() -> new BusinessException("ACC_NOT_FOUND", "轉入賬戶不存在"));
if (fromAcc.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException(from, fromAcc.getBalance(), amount);
fromAcc.debit(amount);
toAcc.credit(amount);
accountRepo.save(fromAcc);
accountRepo.save(toAcc);
}
// Controller 全局處理
@ExceptionHandler(InsufficientFundsException.class)
public R handleInsufficient(InsufficientFundsException e) {
return R.fail(400, e.getMessage()); // 直接透出業務訊息
}
異常處理不是「寫 try-catch」,而是設計錯誤傳遞契約、建立分層異常模型、落地監控告警閉環。從「不吞沒異常」開始,到「業務異常碼化」、再到「重試熔斷自動化」,每一步都在降低系統不確定性。下一篇將實戰《Spring Boot 3 + Resilience4j 異常治理實戰:從零構建高可用微服務錯誤處理體系》。