应使用自定义业务异常类(如BusinessException)封装底层异常,保留cause链并面向业务构造消息,区分业务异常与系统异常处理策略,日志记录需完整展开异常链以利排查。
捕获并封装底层异常,核心是让业务层不被技术细节干扰,同时保留足够线索用于排查。关键不在“包得有多深”,而在“包得是否准确、安全、可维护”。
用自定义异常类统一语义
不要直接把 SQLException、IOException 等抛到 service 或 controller 层。应定义轻量级的业务异常基类(如 BusinessException),继承 RuntimeException,并提供带 cause 的构造函数:
- 必须有 public BusinessException(String msg, Throwable cause),否则原始堆栈在构造时就被截断
- 避免只写 new BusinessException("操作失败") —— 这会丢失所有根因信息
- 子类按语义区分即可,比如 UserNotFoundException、InsufficientBalanceException,无需为每个错误码新建异常类
包装时保留 cause,但控制信息粒度
底层异常要传进去,但消息内容要面向业务,而非面向数据库或网络:
- ✅ 正确:throw new BusinessException("订单创建失败,请稍后再试", e)
- ❌ 错误:throw new BusinessException("MySQL connection refused: localhost:3306", e) —— 暴露环境细节,且前端不该看到
- 日志记录用 log.error("create order failed", e),SLF4J 会自动展开整个 cause 链
分清两类异常,处理策略不同
不是所有异常都该被“友好包装”:
立即学习“Java免费学习笔记(深入)”;
-
业务异常(如参数校验失败、库存不足):预期内发生,前端可直接展示 getMessage(),通常不记 ERROR 日志
-
系统异常(如 DB 连接中断、Redis 超时):属于基础设施故障,应记录 ERROR 日志 + 上报监控,前端统一返回“系统繁忙”,不暴露细节
- IO 类异常(如文件读取失败)若属正常流程分支(例如用户上传空文件),可直接处理,不必再包一层 BusinessException
调试时快速定位根本原因
别依赖 e.toString() 查根因,它只显示当前异常的类名和消息:
- 要用 e.getCause() 逐层获取,直到返回 null;Java 7+ 可配合 ExceptionUtils.getRootCause(e)(Apache Commons)
- 避免手动拼接日志时只写 e.getMessage() —— 可能为空、被重写或截断
- 如果需提取关键字段(如 SQL 状态码、HTTP 状态),应在包装时主动解析并存入异常 context,而不是靠堆栈倒推