Logback 中间出现 log.error("msg " + e) 这种写法时,堆栈信息确实会丢失。出问题的根子在于,这行代码把异常对象 e 给“字符串化”了——它调用了 e.toString(),结果只保留了异常类名和那一行 message,而完整的堆栈跟踪(stack trace)根本没机会传进 Logback 的日志方法。

确认是否真的丢了堆栈
不妨先检查一下实际输出的日志长什么样:
- 如果只有一行类似
ERROR ... msg ja va.lang.NullPointerException: null,没有多行的at xxx.xxx.xxx,那堆栈基本可以确定是丢了; - 如果日志里异常调用链完整,那问题就不在这,得去查配置或者异步日志之类的原因。
正确写法:把异常对象作为最后一个参数传入
Logback(以及 SLF4J)有个约定:当方法签名末尾是 Throwable 类型参数时,框架会自动提取并打印完整堆栈。这个结构必须保持,不能破坏。那么,该如何写才对?
- ✅ 正确:
log.error("msg", e); - ✅ 正确(带格式化):
log.error("user {} failed with code {}", userId, errorCode, e); - ❌ 错误:
log.error("msg " + e);(e 被 toString(),堆栈丢弃) - ❌ 错误:
log.error("msg", e.toString());(传的是字符串,不是 Throwable)
检查日志配置是否禁用了堆栈输出
代码写对了,不代表就万事大吉了。Logback 配置也可能把堆栈给“过滤”掉。重点要看 或 中是否用了自定义 Pattern,尤其要注意几个细节:
- 避免用
%ex、%xEx、%throwable以外的占位符来“手动拼接”异常(比如用%m拼接后加e.toString()); - 确认 pattern 包含
%ex(推荐)或%xEx(带上下文),例如:%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n%ex; - 如果用了
AsyncAppender,得确保它的includeCallerData或异常处理逻辑没有被覆盖(默认情况下不影响堆栈打印)。
排查工具与辅助手段
如果想快速验证当前的行为,可以这么做:
- 在本地写个最小复现:
try { throw new RuntimeException("test"); } catch (Exception e) { log.error("boom", e); },看看控制台或文件输出的结果里有没有堆栈; - 开启 SLF4J 绑定检测:启动时加 JVM 参数
-Dslf4j.detectLoggerNameMismatch=true,能捕获部分绑定异常; - 临时改用
log.error("msg", e.getCause() != null ? e.getCause() : e);排查一下是否因为嵌套异常导致显示不全(不过通常这不是主因)。
问题本身并不复杂,但确实容易忽略一个关键点:异常堆栈不是“自动附带”的,它依赖 SLF4J 的参数类型识别机制。只要保证 Throwable 是独立参数、配置中又启用了 %ex,就能稳定输出完整堆栈。