关键在于启用日志框架的递归展开支持,需使用带throwable参数的日志方法(如logger.error("msg", e))、配置pattern含%ex或%throwable{full},并确保异常包装时传递cause。

在日志框架中完整打印嵌套异常(即由 getCause() 逐层关联的异常链),关键不是“手动遍历”,而是启用日志框架对异常栈迹的**递归展开支持**。多数主流日志框架(如 Logback、Log4j2)默认已支持,但需确保配置正确、日志方法调用得当。
确保使用带 Throwable 参数的日志方法
这是最常见被忽略的一点:如果只传消息字符串,异常对象不会被解析,自然不打印 cause 链。
- ✅ 正确写法(以 SLF4J + Logback 为例):
logger.error("数据库查询失败", e);—— 框架自动递归打印整个异常树 - ❌ 错误写法:
logger.error("数据库查询失败: " + e.getMessage());—— 只输出一句话,无栈迹,更无 cause - ⚠️ 补充:若需拼接上下文又保留异常,用占位符:
logger.error("用户 {} 查询订单时失败", userId, e);
检查日志框架是否启用异常递归展开
Logback 和 Log4j2 默认开启,但个别自定义 PatternLayout 或封装工具类可能禁用或截断。
- Logback 中,
%ex(或%xEx)会完整打印 cause 链;%throwable等价于%ex;%shortException则只打最外层 - 确认你的
logback-spring.xml或logback.xml中 pattern 包含了%ex,例如:<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%ex%n</pattern> - Log4j2 同理,
%throwable{full}或%xThrowable支持递归,避免只用%throwable{short}
警惕被“吞掉”异常的中间层
有些代码会捕获异常后新建一个异常但未设 cause,或用了 new RuntimeException(msg) 而非 new RuntimeException(msg, e),导致链断裂。
- 检查所有
catch块中的异常包装逻辑:
✅throw new ServiceException("操作失败", e);
❌throw new ServiceException("操作失败"); - Spring 的
@ExceptionHandler或全局异常处理器中,也要确保 re-throw 或 log 时传入原始异常对象,而非仅 message
调试时快速验证异常树是否完整
临时加一行代码直接打印,绕过日志框架验证原始异常结构:
-
e.printStackTrace();—— JVM 默认递归打印到System.err,可直观看到 cause 链层级 - 或用工具方法遍历并打印(仅调试用):
while (e != null) { System.out.println(e); e = e.getCause(); } - 如果
printStackTrace()能看到多层,但日志里只有一层 → 问题一定出在日志调用方式或 layout 配置










