《編寫高質量代碼》第 8 章聚焦異常處理,建議 110~113 涵蓋異常封裝、異常鏈傳遞、受檢轉非受檢、finally 禁 return四大核心原則。本文結合 Java 8+ 新特性(try-with-resources、異常多捕獲、模塊化異常體系)進行現代化解讀與工程落地指導。
EntityNotFoundException: ID=1001。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); // 自定義聚合異常
// ... 正常註冊邏輯
}
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", "系統繁忙");
}
}
catch (Exception e) { log.error("", e); throw new BizException(..., e); } 必須保留 cause。try-catch 包裹每行調用,業務邏輯被樣板代碼淹沒。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 + 返回值變量