throw 是 Java 主動拋出異常的唯一關鍵字,配合 throws 聲明、try-catch 捕獲、自定義異常體系,構成了 Java 健壯錯誤處理的骨架。本文從語法細節、異常鏈保護、業務異常設計、效能考量四個維度,系統講解 throw 的正確打開方式。
public void validateAge(int age) {
if (age < 0) {
throw new IllegalArgumentException("年齡不能為負數:" + age);
}
if (age > 150) {
throw new IllegalArgumentException("年齡超過合理範圍:" + age);
}
}
throw 後必須跟 Throwable 子類實例(new XxxException(...))。catch 或向上傳播。public void readConfig(File file) throws ConfigException {
try (FileInputStream fis = new FileInputStream(file)) {
// 解析邏輯
} catch (IOException e) {
// ❌ 錯誤:new Exception(e) 會丟失原始堆疊
// ✅ 正確:直接 throw e,或包裝保留 cause
throw new ConfigException("讀取配置失敗:" + file.getAbsolutePath(), e);
}
}
鐵律:永遠保留 cause,否則生產排查無從下手。
| 維度 | 受檢異常 (extends Exception) | 非受檢異常 (extends RuntimeException) |
|---|---|---|
| 編譯強制 | 必須 throws 或 catch | 可選 |
| 適用場景 | 可預期、可恢復的業務狀態(檔案不存在、資料庫死鎖、第三方服務降級) | 程式錯誤(參數非法、空指針、狀態非法)、不可恢復系統故障 |
| API 契約 | 方法簽名顯式聲明,調用者**知情必處理** | 隱式傳播,調用者**可選擇性處理** |
| 濫用後果 | 層層 throws 污染介面、try-catch 濫用掩蓋真錯 | 關鍵錯誤被吞沒、無感知崩潰 |
BizException extends Exception(受檢)。IllegalArgumentException、IllegalStateException 等標準非受檢異常。catch (SQLException e) { throw new DataAccessException("查詢失敗", e); } —— 統一轉為業務受檢異常,屏蔽實現細節。// 基礎業務異常(受檢)
public class BizException extends Exception {
private final String errorCode;
public BizException(String errorCode, String message) { super(message); this.errorCode = errorCode; }
public BizException(String errorCode, String message, Throwable cause) { super(message, cause); this.errorCode = errorCode; }
public String getErrorCode() { return errorCode; }
}
// 具體業務異常
public class UserNotFoundException extends BizException {
public UserNotFoundException(String username) {
super("USER_NOT_FOUND", "用戶不存在:" + username);
}
}
public class InsufficientBalanceException extends BizException {
public InsufficientBalanceException(long accountId, BigDecimal required, BigDecimal actual) {
super("INSUFFICIENT_BALANCE", String.format("賬戶 %d 餘額不足:需 %s,實 %s", accountId, required, actual));
}
}
// 系統級非受檢異常
public class SystemUnavailableException extends RuntimeException {
public SystemUnavailableException(String system, Throwable cause) {
super("下游系統 " + system + " 不可用", cause);
}
}
MODULE_BIZ_ERROR,如 ORDER_PAY_TIMEOUT、USER_PHONE_DUPLICATE。@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(BizException.class)
public R handleBiz(BizException e) {
log.warn("業務異常:{} - {}", e.getErrorCode(), e.getMessage());
return R.fail(e.getErrorCode(), e.getMessage());
}
@ExceptionHandler(ConstraintViolationException.class)
public R handleValidation(ConstraintViolationException e) {
String msg = e.getConstraintViolations().stream()
.map(v -> v.getPropertyPath() + " " + v.getMessage())
.collect(Collectors.joining("; "));
return R.fail("PARAM_INVALID", msg);
}
@ExceptionHandler(Exception.class)
public R handleUnknown(Exception e) {
log.error("系統未知異常", e); // 完整堆疊入日誌
return R.fail("SYSTEM_ERROR", "系統繁忙,請稍後重試");
}
}
public final class Asserts {
public static void isTrue(boolean expr, String errorCode, String msg) {
if (!expr) throw new BizException(errorCode, msg);
}
public static void notNull(Object obj, String errorCode, String msg) {
if (obj == null) throw new BizException(errorCode, msg);
}
public static void notEmpty(Collection> coll, String errorCode, String msg) {
if (coll == null || coll.isEmpty()) throw new BizException(errorCode, msg);
}
}
// 使用
Asserts.notNull(user, "USER_NOT_FOUND", "用戶不存在");
Asserts.isTrue(balance.compareTo(amount) >= 0, "INSUFFICIENT_BALANCE", "餘額不足");
SQLException (受檢)
│
▼ catch → throw new DataAccessException("查詢失敗", e) // 受檢業務異常
│
▼ ControllerAdvice 捕獲 BizException 子類
│
▼ 映射表:
USER_NOT_FOUND → 404 Not Found
INSUFFICIENT_BALANCE → 409 Conflict
DATA_ACCESS_ERROR → 500 Internal Server Error (並告警)
SYSTEM_UNAVAILABLE → 503 Service Unavailable (並熔斷)
return Result.fail(...) 而非拋異常控制流。if (user == null) throw ... 可改 Optional / Result 模式。serialVersionUID:跨 JVM 傳遞異常(RMI、分布式隊列)必須,否則反序列化失敗。log.error("msg", e) 而非 log.error(e.getMessage())。| 檢查項 | 標準 |
|---|---|
| 異常類型選擇 | 可恢復業務 → 受檢;Bug/不可恢復 → 非受檢 |
| 錯誤碼 | 有、唯一、可國際化、前端可據此決策 |
| Cause 保留 | 所有包裝/轉換必須 super(msg, cause) |
| 堆疊完整性 | 不丟失、不重複填充、日誌完整記錄 |
| 熱路徑無異常 | 高頻調用路徑無 new Exception、無控制流異常 |
| 序列化就緒 | 跨進程異常類有 serialVersionUID |
把這份清單貼在顯示器邊,每次 throw new XxxException 時心裡過一遍——好異常設計,是系統可觀測、可運維、可演進的基石。