应包装而非直接抛出SQLException或IOException,因其污染上层接口、暴露技术细节、破坏抽象且难以恢复;推荐用自定义RuntimeException(如DataAccessException)包装并保留cause,区分BusinessException与SystemException,利用Spring异常翻译体系减少手动处理。

为什么不能直接抛出 SQLException 或 IOException
Java 应用分层(如 Controller → Service → DAO)时,底层异常类型会污染上层接口契约。比如 Service 方法声明抛出 SQLException,就强制调用方感知数据库细节,违背封装原则。更严重的是,这类异常通常未声明为 RuntimeException,导致编译期强制处理,却无法在业务逻辑中合理恢复。
- 暴露技术栈:调用方看到
com.mysql.cj.jdbc.exceptions.CommunicationsException就知道你用了 MySQL - 破坏抽象:Service 层不该承诺“我用 JDBC”,只应承诺“我可能查不到数据”或“操作失败”
- 日志冗余:原始异常堆栈常含连接池、驱动内部调用,干扰问题定位
用 RuntimeException 包装并保留原始异常(cause)
最常用且推荐的方式是继承 RuntimeException 自定义异常类,并在构造器中传入原始异常作为 cause。JVM 会自动将 cause 输出到堆栈末尾,既隐藏底层细节,又不丢失根因。
public class DataAccessException extends RuntimeException {
public DataAccessException(String message, Throwable cause) {
super(message, cause);
}
}
在 DAO 层捕获后包装:
try {
return jdbcTemplate.queryForObject(sql, rowMapper, id);
} catch (EmptyResultDataAccessException e) {
throw new BusinessException("用户不存在", e);
} catch (DataAccessException e) {
throw new DataAccessException("查询用户失败", e);
}
- 不要丢弃
cause:写成new DataAccessException("xxx")会丢失原始堆栈 - 避免重复包装:若已捕获
DataAccessException,再包一层同类型无意义 - 消息尽量业务化:“数据库连接超时” 是错的,“请稍后重试” 更合适
区分包装策略:BusinessException vs SystemException
不是所有异常都该用同一类包装。业务异常(如参数校验失败、余额不足)应可预期、可重试、可友好提示;系统异常(如 DB 连接中断、Redis 超时)则代表基础设施故障,通常不可控、需告警、不应暴露给前端。
立即学习“Java免费学习笔记(深入)”;
-
BusinessException:继承RuntimeException,用于明确的业务规则拒绝,前端可直接展示getMessage() -
SystemException:也继承RuntimeException,但默认记录 ERROR 日志 + 上报监控,前端统一返回“系统繁忙” - 不要用
Exception(受检异常)包装:它迫使上层throws或try-catch,而现代 Spring Web 已通过@ControllerAdvice统一处理
Spring 中如何避免手动 try-catch 包装
Spring 的 org.springframework.dao 异常体系本身已是包装好的抽象层(如 DataAccessException),DAO 操作抛出的原生 JDBC 异常会被自动转换。你只需配置 DataSourceTransactionManager 或使用 JdbcTemplate,就能获得统一的、非技术栈绑定的异常类型。
- 启用
SQLExceptionTranslator:JdbcTemplate 默认使用SQLStateSQLExceptionTranslator,把 MySQL 的 1045 错误转为InvalidIsolationLevelException - 自定义翻译器:继承
SQLExceptionTranslator,针对特定 SQLState 返回更精准的子类 - 注意陷阱:MyBatis 默认不走 Spring 的异常翻译链,需配置
exceptionTranslator或手动包装
GlobalExceptionHandler,结果日志里只剩 BusinessException: 系统错误 —— 原始异常被吞了,堆栈截断了,连是 Redis 还是 DB 出问题都得翻三遍日志。










