异常包装的核心目标是剥离底层技术细节,通过语义转换将sqlexception等转为业务明确的runtimeexception,在分层边界精准转换、消息去技术化、日志与响应分离,并结合装饰器等设计模式强化隔离。

异常包装的核心目标,就是把底层技术细节(比如数据库驱动名、文件路径、网络超时机制)从上层业务逻辑中彻底拿掉。它不是简单地“换一个异常类”,而是通过语义转换和信息裁剪,让调用方只关心“发生了什么业务问题”,而不是“哪一行代码、哪个JDBC驱动、哪张表出错了”。
用自定义 RuntimeException 封装,不暴露检查型异常
DAO 层执行 SQL 时抛出 SQLException,Service 层不能直接 throws 它——这等于告诉调用方:“我用了 MySQL,连的是 192.168.1.100,查的是 user_info 表”。正确做法是捕获后转成业务语义明确的运行时异常:
- 继承
RuntimeException,避免强制上层 try-catch - 构造函数必须接收
Throwable cause,确保原始堆栈可追溯 - 消息写成“用户查询失败”,而不是“Connection refused: connect”
- 不要丢弃 cause:
new DataAccessException("加载用户失败", e)✅;new DataAccessException("加载用户失败")❌
在分层边界做一次干净剥离
异常包装不是处处都包,关键是在架构层边界(如 DAO → Service、Service → Controller)做一次精准转换:
- DAO 层捕获
SQLException/IOException,统一包装为DataAccessException或IoException - Service 层再根据业务含义进一步转为
UserNotFoundException、InsufficientBalanceException等 - Controller 层只面对这些业务异常,由全局处理器统一返回 HTTP 状态码和 JSON 错误体
- 绝不出现跨层传递原始 JDBC 异常,也不在 Service 方法签名里声明
throws SQLException
消息内容去技术化,日志与响应分离
同一个异常,在日志里可以保留部分上下文用于排查,在对外响应中必须脱敏:
- 日志记录:用
log.error("订单支付失败 | userId={}, orderId={}", userId, orderId, e)—— 保留关键业务标识 + 完整异常链 - API 响应:只返回
{"code": 4001, "message": "余额不足,请充值"},不带堆栈、不带 cause、不拼接数据库字段名 - 敏感字段如密码、token、完整 SQL、服务器路径,一律不出现在 message 字符串中
- 若需调试,可在内部日志中单独打印
e.getCause().toString(),但生产环境关闭该行
配合设计模式强化隔离效果
异常包装不是孤立技巧,它和设计模式协同才能真正切断耦合:
- 装饰器模式中,在装饰层统一捕获并重包装,使被装饰对象的异常对调用方不可见
- 策略模式里,各策略实现内部消化各自 SDK 异常(微信支付异常、银联异常),对外只抛
PaymentException - 模板方法中,子类抛
ValidationException,模板统一处理并跳过后续步骤,不把校验失败和 IO 失败混为一谈 - 观察者模式中,单个观察者异常被捕获并记录,不中断其他观察者执行,也不向上透传具体异常类型
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











