受检异常包装本质是类型转换而非简单套壳,需保留原始堆栈、语义清晰、不丢失上下文;必须用带throwable构造参数的方式包装,显式声明构造器,按sql状态码/错误码分类转换,日志须在包装前用支持多级cause的方式记录,全局处理器避免二次包装,对外api禁止暴露原始sql或堆栈。

受检异常包装的本质是类型转换,不是简单套壳
Java 中 SQLException 是受检异常(checked exception),而多数业务层希望抛出非受检的 BusinessException(继承自 RuntimeException)。所谓“包装”,不是用新异常把旧异常塞进去就完事——关键在于保留原始堆栈、语义清晰、不丢失上下文。
常见错误是直接 new 一个新异常但没传 cause:throw new BusinessException("数据库操作失败")。这会丢掉 SQLException 的 SQL 状态码、错误码、具体 SQL 片段等诊断信息,后续排查只能靠猜。
- 必须用带
Throwable构造参数的重载:new BusinessException("订单查询失败", e) - 业务异常类需显式声明接收
Throwable的构造器,否则编译报错 - 不要在包装时吞掉原始异常(即避免
catch { log; throw new BusinessException(...) }后不 re-throw 或不设 cause)
封装前先做分类判断,避免“一刀切”包装
不是所有 SQLException 都该转成同一个 BusinessException。比如连接超时、唯一键冲突、SQL 语法错误,业务含义和用户提示策略完全不同。
典型场景:插入用户时触发主键/唯一索引冲突,应转为 DuplicateKeyException(可继承自 BusinessException),前端可明确提示“手机号已被注册”;而连接池耗尽则属于系统级故障,更适合转为 SystemUnavailableException 并触发降级逻辑。
- 用
e.getSQLState()或e.getErrorCode()做分支判断(如 PostgreSQL 的23505表示唯一约束违规) - 避免仅靠
e.getMessage()字符串匹配——不同驱动返回格式不一致,不可靠 - Spring JDBC 已内置部分分类(如
org.springframework.dao.DuplicateKeyException),可直接复用或参考其实现
日志记录必须在包装前完成,且带完整 cause
很多人习惯在 catch 块里先 log,再 throw 新异常。但如果 log 时只打 e.getMessage(),或用了不支持递归打印 cause 的日志框架(如老版本 Log4j 1.x 默认不展开嵌套异常),关键线索就没了。
正确做法是在包装前,用支持多级 cause 的方式记录原始异常,例如 SLF4J 的 logger.error("DB query failed", e) —— 这会自动展开整个异常链。
- 禁止写
logger.error("DB query failed: " + e.getMessage()),这是最常踩的坑 - 若用 Logback,确认
%ex或%xExpattern 被启用;Log4j2 则检查%throwable{full} - 生产环境建议开启 JDBC 驱动的 trace 日志(如 HikariCP 的
leakDetectionThreshold配合jdbc:logging),辅助定位连接泄漏类问题
全局异常处理器里别二次包装,否则 cause 层级被破坏
Spring 的 @ControllerAdvice 或 WebFlux 的全局异常处理中,如果对已包装过的 BusinessException 再次 new 一个新异常(比如统一加个 errorId),会导致原始 SQLException 被埋得更深,堆栈里出现两层甚至三层 “Caused by”,调试时容易看花眼。
真正该做的,是在全局处理器里识别出业务异常子类,提取原始 cause(e.getCause()),然后有选择地透出底层错误码或 SQL 片段给监控系统,而不是再套一层。
- 用
instanceof区分是否已是业务异常,避免重复封装 - 全局 handler 中调用
e.getRootCause()(Spring 提供的工具方法)比循环getCause()更安全 - 对外 API 返回体里,业务异常可暴露
errorCode和message,但绝不暴露原始 SQL 或堆栈(防信息泄露)










