首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 異常拋出最佳實踐:何時拋、如何拋、為什麼要拋自定義異常

     編輯:Java 相關知識

認知轉變:異常是朋友,不是敵人

初學者以為 throw 是「製造錯誤」,實則是主動暴露問題:

  • 默認錯誤信息模糊 (如 NullPointerException 無上下文)
  • 錯誤發生位置與發現位置分離,調試困難
  • 主動拋出帶上下文的異常,讓調試「直達現場」

核心原則

  1. 只在已知會出錯的地方拋出:參數校驗、資源不存在、業務規則違反
  2. 拋出信息豐富的異常:包含「什麼操作、什麼參數、為什麼失敗、建議怎麼修」
  3. 區分業務異常 vs 系統異常:前者可預期可恢復,後者需運維介入
  4. 庫/框架代碼必須拋出清晰異常:調用者無法深入內部排查

反面教材 vs 正面範例

❌ 糟糕:靜默失敗或拋出原始異常

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);
}

異常類型選擇指南

場景推薦異常說明
參數非法IllegalArgumentExceptionJDK 內建,語義明確
狀態非法 (如未初始化調用)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()));
    }
}

try-catch 策略:捕獲處理 vs 向上拋出

層級策略示例
庫/框架/工具類拋出,不捕獲 (或捕獲轉換語義)DAO 拋 DataAccessException
Service 業務邏輯捕獲轉換:將技術異常包裝為業務異常catch SQLException → throw BusinessException
Controller / 入口統一攔截:@ControllerAdvice 映射到 HTTP 響應/錯誤頁BusinessException → 400 + errorCode
異步/批量任務捕獲記錄+補償:不拋出導致任務失敗,記錄日誌、入死信隊列@Retryable + DeadLetterQueue

禁忌清單

  • ❌ catch (Exception e) { log.error(e); return null; } —— 吞噬異常,調用方拿到 null 產生 NPE
  • ❌ catch (Exception e) { throw new RuntimeException(e); } —— 無語義包裝,堆棧雜訊
  • ❌ finally { return result; } —— 吞噬 try/catch 中的異常 (見文章 131742)
  • ❌ 空 catch 塊 —— 掩蓋錯誤,違背異常機制初衷
  • ❌ 用異常控制正常流程 (如循環終止) —— 性能差、語義混淆

總結

合理的異常拋出是「在正確的位置、拋出正確的類型、攜帶正確的上下文」。遵循:參數校驗用 JDK 內建異常、業務邏輯用自定義 BusinessException、系統故障用 SystemException、Controller 統一攔截轉換響應。將異常視為「帶上下文的錯誤報告」,而非「程序崩潰的信號」,代碼將更健壯、可維護、易調試。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved