初學者以為 throw 是「製造錯誤」,實則是主動暴露問題:
NullPointerException 無上下文)function addClass(element, className) {
element.className += " " + className; // element 為 null 時拋 NPE,信息極少
}
public void addClass(Element element, String className) {
if (element == null || element.getClassName() == null) {
throw new IllegalArgumentException(
"addClass(): element 參數不能為 null,且必須支援 className 屬性。" +
"傳入值: element=" + element + ", className=" + className
);
}
element.setClassName(element.getClassName() + " " + className);
}
| 場景 | 推薦異常 | 說明 |
|---|---|---|
| 參數非法 | IllegalArgumentException | JDK 內建,語義明確 |
| 狀態非法 (如未初始化調用) | IllegalStateException | 對象狀態不滿足前置條件 |
| 資源不存在 | NoSuchElementException 或自定義 NotFoundException | 查詢無結果 |
| 業務規則違反 | 自定義 BusinessException extends RuntimeException | 攜帶錯誤碼、國際化鍵、上下文數據 |
| 系統級故障 (DB 掛了、網絡斷了) | 自定義 SystemException extends RuntimeException | 需告警、運維介入,不可預期恢復 |
public abstract class AppException extends RuntimeException {
private final String errorCode; // 業務錯誤碼:USER_NOT_FOUND, PAY_FAILED
private final Map context; // 上下文數據:userId, orderId, requestParams
protected AppException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
protected AppException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
public AppException addContext(String key, Object value) {
context.put(key, value);
return this; // 鏈式調用
}
// getter 省略
}
// 業務異常
public class BusinessException extends AppException {
public BusinessException(String errorCode, String message) { super(errorCode, message); }
public BusinessException(String errorCode, String message, Throwable cause) { super(errorCode, message, cause); }
}
// 系統異常
public class SystemException extends AppException {
public SystemException(String errorCode, String message) { super(errorCode, message); }
public SystemException(String errorCode, String message, Throwable cause) { super(errorCode, message, cause); }
}
@Service
public class OrderService {
public Order createOrder(CreateOrderCmd cmd) {
// 1. 參數校驗 → IllegalArgumentException
if (cmd.getUserId() == null) {
throw new IllegalArgumentException("createOrder: userId 不能為空");
}
// 2. 業務校驗 → BusinessException
User user = userRepo.findById(cmd.getUserId());
if (user == null) {
throw new BusinessException("USER_NOT_FOUND", "用戶不存在")
.addContext("userId", cmd.getUserId());
}
if (user.getStatus() == UserStatus.FROZEN) {
throw new BusinessException("USER_FROZEN", "用戶已凍結,無法下單")
.addContext("userId", user.getId()).addContext("status", user.getStatus());
}
// 3. 外部依賴失敗 → SystemException (保留原始堆棧)
try {
PaymentResult pay = paymentClient.charge(cmd.getAmount());
if (!pay.isSuccess()) {
throw new BusinessException("PAY_FAILED", "支付失敗: " + pay.getMsg())
.addContext("payId", pay.getId());
}
} catch (PaymentGatewayException e) {
throw new SystemException("PAYMENT_GATEWAY_ERROR", "支付網關異常", e)
.addContext("amount", cmd.getAmount());
}
// 4. 正常創建
return orderRepo.save(new Order(user, cmd.getItems()));
}
}
| 層級 | 策略 | 示例 |
|---|---|---|
| 庫/框架/工具類 | 拋出,不捕獲 (或捕獲轉換語義) | DAO 拋 DataAccessException |
| Service 業務邏輯 | 捕獲轉換:將技術異常包裝為業務異常 | catch SQLException → throw BusinessException |
| Controller / 入口 | 統一攔截:@ControllerAdvice 映射到 HTTP 響應/錯誤頁 | BusinessException → 400 + errorCode |
| 異步/批量任務 | 捕獲記錄+補償:不拋出導致任務失敗,記錄日誌、入死信隊列 | @Retryable + DeadLetterQueue |
catch (Exception e) { log.error(e); return null; } —— 吞噬異常,調用方拿到 null 產生 NPEcatch (Exception e) { throw new RuntimeException(e); } —— 無語義包裝,堆棧雜訊finally { return result; } —— 吞噬 try/catch 中的異常 (見文章 131742)catch 塊 —— 掩蓋錯誤,違背異常機制初衷合理的異常拋出是「在正確的位置、拋出正確的類型、攜帶正確的上下文」。遵循:參數校驗用 JDK 內建異常、業務邏輯用自定義 BusinessException、系統故障用 SystemException、Controller 統一攔截轉換響應。將異常視為「帶上下文的錯誤報告」,而非「程序崩潰的信號」,代碼將更健壯、可維護、易調試。