dao层异常需通过异常链封装为业务异常并保留cause,统一处理器中日志记录须传入完整异常对象,避免拼接消息或丢弃cause,spring的dataaccessexception层次支持精准转换。

DAO层抛出的数据库异常(如 SQLException、DataAccessException)通常与具体技术强耦合,不应直接暴露给服务层。利用异常链(Exception Chaining),可以在封装新异常时保留原始异常的完整堆栈和上下文,实现清晰、可追溯的异常转换。
使用带 cause 参数的构造器封装异常
这是最直接、标准的做法。服务层捕获 DAO 异常后,创建一个业务语义明确的服务层异常(如 UserServiceException),并将原始异常作为 cause 传入其构造器:
- 确保自定义异常类提供
Throwable cause构造器(继承自RuntimeException或Exception时,父类通常已支持) - 不要只用字符串拼接“隐藏”原始异常,必须显式传递
cause - 示例:
throw new UserServiceException("用户保存失败", e);—— 此处e是捕获的DataAccessException
统一异常处理器中保留原始堆栈
在 Spring 等框架中,常通过 @ExceptionHandler 统一处理服务层异常。此时需注意:
- 不要调用
e.getMessage()重新构造异常,否则丢失 cause 链 - 日志记录应使用
log.error("业务操作异常", e)(而非log.error("业务操作异常: " + e.getMessage())),以输出完整异常链 - 若需补充上下文信息(如用户ID、操作类型),可用
new ServiceException("用户[123]更新失败", e),而非覆盖或丢弃 cause
避免常见破坏异常链的操作
以下做法会切断异常链,导致根因丢失:
- 仅 throw 新异常且不传 cause:
throw new ServiceException("数据库操作异常"); - 手动拼接消息并忽略原始异常:
throw new ServiceException("DB error: " + e.getClass().getSimpleName()); - 捕获后吞掉异常(空 catch 块)或只打印日志却不 re-throw
- 将 checked 异常转为 unchecked 时未保留 cause(如
new RuntimeException(e)是安全的,但new RuntimeException(e.toString())不是)
配合 Spring 的 DataAccessException 层次结构更精准转换
Spring JDBC/ORM 模块已将底层数据库异常(如 MySQL 的 SQLIntegrityConstraintViolationException)统一抽象为语义化子类(如 DuplicateKeyException、DataIntegrityViolationException)。服务层可据此做差异化处理:
- 捕获
DuplicateKeyException→ 转为UserAlreadyExistsException - 捕获
EmptyResultDataAccessException→ 转为UserNotFoundException - 捕获通用
DataAccessException→ 转为泛化DataServiceException - 所有转换均保持
cause不变,便于运维定位真实数据库错误
异常链不是锦上添花,而是保障可观测性的基础机制。只要每次封装都坚持传 cause,并在日志和传播中尊重它,就能让服务层异常既面向业务、又不失根因线索。










