Java异常处理实践:日志、异常链与业务记录的正确姿势
异常处理的核心目标不是“捕获异常”,而是:让问题在日志和业务系统中可以被快速定位。
在实际业务开发中,经常会遇到这样的代码:
1 | try { |
很多人会问:
- 这样写是不是 重复日志 ?
- 是否应该把
e.getMessage()拼接到新的异常里? - 如果需要把异常写入 业务表单 ,应该如何处理?
本文总结一套比较稳定的工程实践。
一、不要嵌套拼接异常 message
很多代码会这样写:
1 | throw new RuntimeException( |
如果下层接口也做类似处理,最终日志会变成:
1 | Oms工单执行失败: [订单同步失败: [库存服务失败: [timeout]]] |
问题:
- message 层层嵌套
- 可读性越来越差
- 信息重复
实际上 Java 已经提供了更好的机制: 异常链(Cause Chain) 。
正确写法:
1 | throw new RuntimeException("Oms工单执行失败", e); |
日志会自动展示调用链:
1 | java.lang.RuntimeException: Oms工单执行失败 |
优点:
- 每层异常语义清晰
- 不需要拼接 message
- stacktrace 自然表达调用关系
二、避免重复记录日志
很多代码在每一层都 log.error :
1 | catch (Exception e) { |
如果上层也记录日志,就会出现:
1 | ERROR xxx failed |
同一异常 打印两次 stacktrace 。
推荐规则:
异常只在最外层统一记录日志
典型结构:
1 | Controller / Job / MQ Consumer |
中间层只负责:
1 | throw new RuntimeException("业务失败", e); |
三、什么时候需要在中间层 log
如果你需要 记录额外业务上下文 ,那就应该记录日志。
例如:
1 | catch (Exception e) { |
因为这里补充了:
1 | workOrderId |
这类信息在上层可能无法获取。
四、如果需要把异常写入业务表
在工单系统 / 调度系统 / 订单系统中,经常需要把异常写入数据库,例如:
1 | work_order |
推荐只记录 最有价值的一层异常 ,通常是 root cause 。
示例:
1 | String errorMsg = "Oms工单执行失败: " + getRootMessage(e); |
实现:
1 | public static String getRootMessage(Throwable e) { |
结果:
1 | Oms工单执行失败: timeout |
这样不会产生嵌套污染。
五、使用 Hutool 简化异常处理
如果项目使用了 Hutool ,可以直接使用工具方法。
获取 root cause message:
1 | ExceptionUtil.getRootCauseMessage(e); |
获取 root cause:
1 | ExceptionUtil.getRootCause(e); |
获取完整 stacktrace:
1 | ExceptionUtil.stacktraceToString(e); |
推荐写法:
1 | String errorMsg = "Oms工单执行失败: " |
六、完整示例
1 | try { |
日志:
1 | RuntimeException: Oms工单执行失败 |
数据库:
1 | Oms工单执行失败: timeout |
七、一套简单的异常处理原则
总结为四条:
1️⃣ 不要拼接异常 message
依赖异常链表达调用关系。
2️⃣ 只在最外层记录 error 日志
避免重复 stacktrace。
3️⃣ 业务表只记录 root cause
保证信息简洁。
4️⃣ 异常 message 只表达当前层语义
例如:
1 | Oms工单执行失败 |
每一层职责清晰。
总结
异常处理的核心目标不是“捕获异常”,而是:
让问题在日志和业务系统中可以被快速定位。
一套干净的异常策略通常包含:
- 异常链表达调用关系
- 日志只记录一次
- 业务表记录 root cause
- message 只描述当前层语义
这样当系统出现问题时,排查路径会非常清晰。