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

編寫高質量代碼:改善 Java 程序的 151 個建議——第 8 章 異常處理(建議 110~113)深度解析與現代化實踐

     編輯:Java 綜合教學

前言:異常是「非預期」的契約,而非錯誤碼

《編寫高質量代碼》第 8 章聚焦異常處理,建議 110~113 涵蓋異常封裝、異常鏈傳遞、受檢轉非受檢、finally 禁 return四大核心原則。本文結合 Java 8+ 新特性(try-with-resources、異常多捕獲、模塊化異常體系)進行現代化解讀與工程落地指導。

建議 110:提倡異常封裝——從「天書」到業務語言

三大核心價值

  1. 提升系統友好性:用戶看到「訂單號不存在」而非 EntityNotFoundException: ID=1001。
  2. 提升可維護性:運維人員根據業務異常碼快速定位,無需翻閱堆棧。
  3. 解決 Java 單一異常拋出限制:聚合多個驗證錯誤一次性返回(如表單提交)。

反模式:直接拋出底層異常

public void loadConfig() throws FileNotFoundException { // 直接拋出
    new FileInputStream("config.properties");
}
// 調用者只能 catch FileNotFoundException,業務含義模糊

正模式:封裝業務異常 + 保留原始堆棧

// 1. 定義業務異常體系(根異常攜帶錯誤碼、用戶提示、原始原因)
public class BizException extends RuntimeException {
    private final String code;      // 業務錯誤碼:USER_NOT_FOUND, ORDER_EXPIRED
    private final String userMsg;   // 給最終用戶看的友好提示

    public BizException(String code, String userMsg, Throwable cause) {
        super(userMsg, cause);      // 保留原始堆棧,便於開發排查
        this.code = code;
        this.userMsg = userMsg;
    }
    // getter 省略
}

// 2. 封裝點:DAO/Client/外部調用邊界
public User findUser(Long id) {
    try {
        return userDao.selectById(id);
    } catch (EmptyResultDataAccessException e) { // Spring JDBC 異常
        throw new BizException("USER_NOT_FOUND", "用戶不存在", e); // 封裝+鏈式
    } catch (DataAccessException e) {
        throw new BizException("DB_ERROR", "系統繁忙,請稍後重試", e);
    }
}

// 3. 聚合多驗證錯誤(解決單一拋出限制)
public void register(UserRegisterCmd cmd) {
    List errors = new ArrayList<>();
    if (userRepo.existsByUsername(cmd.getUsername())) 
        errors.add(new BizException("USERNAME_EXISTS", "用戶名已佔用", null));
    if (!passwordPolicy.valid(cmd.getPassword())) 
        errors.add(new BizException("WEAK_PASSWORD", "密碼強度不足", null));
    if (errors.size() > 0) throw new AggregateBizException(errors); // 自定義聚合異常
    // ... 正常註冊邏輯
}

建議 111:採用異常鏈傳遞異常——責任鏈模式在異常流中的應用

核心機制:Throwable(Throwable cause) 構造器

// 持久層:封裝 SQL 異常
@Repository
public class OrderDao {
    public Order findById(Long id) {
        try { return jdbc.queryForObject(sql, mapper, id); }
        catch (EmptyResultDataAccessException e) { throw new DataNotFoundException("Order", id, e); }
        catch (DataAccessException e) { throw new PersistenceException("查詢訂單失敗", e); }
    }
}

// 業務層:轉換為業務異常,豐富上下文
@Service
public class OrderService {
    public OrderDetail getDetail(Long id) {
        Order order = orderDao.findById(id); // 可能拋 DataNotFound/Persistence
        if (order == null) throw new BizException("ORDER_NOT_FOUND", "訂單不存在");
        return assembleDetail(order);
    }
}

// 控制層:統一異常處理器(ControllerAdvice)映射 HTTP 狀態碼
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BizException.class)
    public R handleBiz(BizException e) {
        log.warn("業務異常: code={}, msg={}", e.getCode(), e.getUserMsg());
        return R.fail(e.getCode(), e.getUserMsg()); // 返回給前端
    }
    @ExceptionHandler(PersistenceException.class)
    public R handleDb(PersistenceException e) {
        log.error("數據庫異常", e); // 完整堆棧記錄日誌
        return R.fail("SYS_ERROR", "系統繁忙");
    }
}

關鍵原則

  1. 不吞噬異常:catch (Exception e) { log.error("", e); throw new BizException(..., e); } 必須保留 cause。
  2. 邊界轉換:每層只處理該層關心的異常,轉換為上層可理解的異常類型。
  3. 展現層決策:管理員可見技術細節,普通用戶僅見友好提示。

建議 112:受檢異常盡可能轉化為非受檢異常——接口穩定與可讀性的博弈

受檢異常三大痛點

  1. 接口脆弱:新增受檢異常 → 所有實現類、調用者強制修改(違反開閉原則)。
  2. 代碼膨脹:try-catch 包裹每行調用,業務邏輯被樣板代碼淹沒。
  3. 開發負擔:層層 throws 傳遞,實際處理往往在最上層統一攔截。

轉換策略:邊界處轉非受檢,核心域保留受檢

場景建議理由
外部依賴調用(DB、RPC、HTTP、文件)捕獲受檢 → 拋非受檢調用者無法在編碼期預知具體失敗模式,統一上層攔截更合理
核心業務規則違反(庫存不足、狀態非法、重複提交)**保留受檢異常**屬於「可預期的業務分支」,調用者**必須**顯式處理(補償、重試、提示)
編程錯誤(參數非法、空指針、邏輯 Bug)非受檢屬於 Bug,應快速失敗,不應被捕獲處理

現代化實踐:自定義非受檢業務異常體系


判斷依據:是否威脅「安全性、穩定性、可靠性、正確性」

若異常代表必須由調用者即時處理的業務分支(如扣款失敗需回滾、鎖獲取失敗需重試),**保留受檢**。
其他場景(外部依賴失敗、配置錯誤、資源耗盡)統一轉非受檢,由全局異常處理器決策降級/熔斷/告警/友好提示。

建議 113:不要在 finally 塊中處理返回值——隱式覆蓋與異常吞噬的雙重陷阱

陷阱 1:覆蓋 try/catch 的返回值

 -1, faulty(-1) -> -1 (異常被吞噬!)

陷阱 2:屏蔽異常(finally 中 return/throw)


陷阱 3:修改基本類型局部變量無效

public Person refVar() {
    Person p = new Person("張三");
    try { return p; }         // 返回引用地址
    finally { p.setName("李四"); } // 修改對象內容,調用者拿到「李四」
    // p = new Person("王五"); // 重新賦值 p 無效(引用已拷貝)
}
// 返回 Person{name='李四'}

正確模式:try-with-resources + 返回值變量

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