Java异常处理实践:日志、异常链与业务记录的正确姿势

异常处理的核心目标不是“捕获异常”,而是:让问题在日志和业务系统中可以被快速定位。


在实际业务开发中,经常会遇到这样的代码:

1
2
3
4
5
6
try {
service.adjustInventory();
} catch (Exception e) {
log.error("ExecuteWmsWorkOrderAppService adjustInventory failed", e);
throw new RuntimeException("Oms工单执行失败" + e.getMessage(), e);
}

很多人会问:

  • 这样写是不是 重复日志
  • 是否应该把 e.getMessage() 拼接到新的异常里?
  • 如果需要把异常写入 业务表单 ,应该如何处理?

本文总结一套比较稳定的工程实践。


一、不要嵌套拼接异常 message

很多代码会这样写:

1
2
3
4
throw new RuntimeException(
String.format("Oms工单执行失败: [%s]", e.getMessage()),
e
);

如果下层接口也做类似处理,最终日志会变成:

1
Oms工单执行失败: [订单同步失败: [库存服务失败: [timeout]]]

问题:

  1. message 层层嵌套
  2. 可读性越来越差
  3. 信息重复

实际上 Java 已经提供了更好的机制: 异常链(Cause Chain)

正确写法:

1
throw new RuntimeException("Oms工单执行失败", e);

日志会自动展示调用链:

1
2
3
4
5
java.lang.RuntimeException: Oms工单执行失败
at ...
Caused by: java.lang.RuntimeException: 订单同步失败
at ...
Caused by: java.net.SocketTimeoutException: timeout

优点:

  • 每层异常语义清晰
  • 不需要拼接 message
  • stacktrace 自然表达调用关系

二、避免重复记录日志

很多代码在每一层都 log.error

1
2
3
4
catch (Exception e) {
log.error("xxx failed", e);
throw new RuntimeException("业务失败", e);
}

如果上层也记录日志,就会出现:

1
2
3
4
5
ERROR xxx failed
stacktrace...

ERROR 业务失败
stacktrace...

同一异常 打印两次 stacktrace

推荐规则:

异常只在最外层统一记录日志

典型结构:

1
2
3
4
5
Controller / Job / MQ Consumer

log.error

throw / return

中间层只负责:

1
throw new RuntimeException("业务失败", e);

三、什么时候需要在中间层 log

如果你需要 记录额外业务上下文 ,那就应该记录日志。

例如:

1
2
3
4
catch (Exception e) {
log.error("工单执行失败, workOrderId={}", workOrderId, e);
throw new RuntimeException("Oms工单执行失败", e);
}

因为这里补充了:

1
workOrderId

这类信息在上层可能无法获取。


四、如果需要把异常写入业务表

在工单系统 / 调度系统 / 订单系统中,经常需要把异常写入数据库,例如:

1
2
3
4
5
6
work_order
---------
id
status
error_msg
error_stack

推荐只记录 最有价值的一层异常 ,通常是 root cause

示例:

1
String errorMsg = "Oms工单执行失败: " + getRootMessage(e);

实现:

1
2
3
4
5
6
7
public static String getRootMessage(Throwable e) {
Throwable root = e;
while (root.getCause() != null) {
root = root.getCause();
}
return root.getMessage();
}

结果:

1
Oms工单执行失败: timeout

这样不会产生嵌套污染。


五、使用 Hutool 简化异常处理

如果项目使用了 Hutool ,可以直接使用工具方法。

获取 root cause message:

1
ExceptionUtil.getRootCauseMessage(e);

获取 root cause:

1
ExceptionUtil.getRootCause(e);

获取完整 stacktrace:

1
ExceptionUtil.stacktraceToString(e);

推荐写法:

1
2
String errorMsg = "Oms工单执行失败: " 
+ ExceptionUtil.getRootCauseMessage(e);

六、完整示例

1
2
3
4
5
6
7
8
9
10
11
try {
service.adjustInventory();
} catch (Exception e) {

String errorMsg = "Oms工单执行失败: "
+ ExceptionUtil.getRootCauseMessage(e);

workOrder.setErrorMsg(errorMsg);

throw new RuntimeException("Oms工单执行失败", e);
}

日志:

1
2
RuntimeException: Oms工单执行失败
Caused by: SocketTimeoutException: timeout

数据库:

1
Oms工单执行失败: timeout

七、一套简单的异常处理原则

总结为四条:

1️⃣ 不要拼接异常 message
依赖异常链表达调用关系。

2️⃣ 只在最外层记录 error 日志
避免重复 stacktrace。

3️⃣ 业务表只记录 root cause
保证信息简洁。

4️⃣ 异常 message 只表达当前层语义

例如:

1
2
3
Oms工单执行失败
订单同步失败
库存服务失败

每一层职责清晰。


总结

异常处理的核心目标不是“捕获异常”,而是:

让问题在日志和业务系统中可以被快速定位。

一套干净的异常策略通常包含:

  • 异常链表达调用关系
  • 日志只记录一次
  • 业务表记录 root cause
  • message 只描述当前层语义

这样当系统出现问题时,排查路径会非常清晰。