異常不僅是錯誤信號,更是調試利器。優秀的異常能回答三個問題:發生什麼?在哪發生?為什麼發生? 本文確立三大核心原則,構建健壯、可維護的異常體系。
Throwable
├── Error (系統級,不可恢復)
└── Exception
├── RuntimeException (非檢查,編程錯誤)
└── IOException (檢查異常,外部環境)
├── FileNotFoundException
├── EOFException
└── ObjectStreamException
// ❌ 糟糕:吞沒具體信息,用戶看到堆棧不知所措
try { readFile(); } catch (Exception e) { log.error(e); }
try {
readPreferences(prefsFile);
} catch (FileNotFoundException e) {
alert("配置文件不存在,請指定正確路徑"); // 可恢復:重新選擇
} catch (EOFException e) {
alert("文件讀取中斷,將使用已讀取部分"); // 可恢復:部分數據
} catch (ObjectStreamException e) {
alert("文件損壞,請恢復備份"); // 需人工介入
} catch (IOException e) {
alert("讀取失敗:" + e.getMessage()); // 兜底
}
public class InsufficientBalanceException extends BusinessException {
private final BigInteger balance;
private final BigInteger required;
// 構造器、getter、錯誤碼...
}
// 使用
if (account.balance.compareTo(amount) < 0) {
throw new InsufficientBalanceException(account.balance, amount);
}
// ❌ 延遲:NullPointerException 無任何業務語義
public void read(String filename) {
InputStream in = new FileInputStream(filename); // filename 為 null 時拋 NPE
// ... 幾十行後才發現
}
// ✅ 快速失敗:參數驗證前置,語義清晰
public void read(String filename) {
if (filename == null) {
throw new IllegalArgumentException("filename cannot be null");
}
InputStream in = new FileInputStream(filename);
// ...
}
// 延遲拋出堆棧
java.lang.NullPointerException
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.(FileInputStream.java:103)
at com.app.Config.read(Config.java:45) ← 真實錯誤源頭被掩蓋
// 快速失敗堆棧
java.lang.IllegalArgumentException: filename cannot be null
at com.app.Config.read(Config.java:12) ← 直指問題本質
at com.app.App.startup(App.java:20)
throws IllegalArgumentException 即是契約// ❌ 低層過早捕獲,導致上層無法恢復
public void readPreferences(String file) {
try {
in = new FileInputStream(file);
} catch (FileNotFoundException e) {
logger.log(e); // 記錄後吞沒
}
in.read(...); // 💥 這裡拋 NPE,誤導調試方向
}
// 數據訪問層:聲明拋出,不捕獲
public void readPreferences(String filename)
throws IllegalArgumentException, FileNotFoundException, IOException {
if (filename == null) throw new IllegalArgumentException("filename is null");
try (InputStream in = new FileInputStream(filename)) {
// 讀取邏輯...
}
}
// 業務層:可選轉換為業務異常
public Preferences loadPrefs(String file) {
try {
dao.readPreferences(file);
} catch (FileNotFoundException e) {
throw new ConfigNotFoundException("配置文件缺失", e); // 保留因果鏈
}
}
// 表現層(Controller/UI):最終捕獲,用戶交互
@GetMapping("/prefs")
public ResponseEntity> getPrefs(@RequestParam String file) {
try {
return ok(service.loadPrefs(file));
} catch (ConfigNotFoundException e) {
return notFound().body(Map.of("error", "請先創建配置文件", "action", "create"));
} catch (IOException e) {
return internalError().body("系統暫時不可用,請稍後重試");
}
}
| 原則 | 核心動作 | 關鍵收益 |
|---|---|---|
| 具體化 | 捕獲/拋出最具體異常類 | 精準定位、用戶友好、可差異化處理 |
| 快速失敗 | 驗證前置、檢測即拋出 | 堆棧直指本質、防資源洩漏、契約文檔化 |
| 延遲捕獲 | 只在能恢復/交互層捕獲 | 保持異常傳播、分層解耦、避免吞沒導致級聯錯誤 |
public 方法驗證參數,拋 IllegalArgumentExceptionRuntimeException (業務) 或 Exception (可恢復)throw new BizEx("msg", cause)catch (Exception e) {} 空捕獲遵循三大鐵律,異常從「麻煩」變為「導航儀」,讓系統在失敗中依然可控、可觀測、可恢復。