Java记录异常首选logger.error("msg", e),因其自动提取完整堆栈、cause链和suppressed异常;需用MDC注入userId等上下文,避免字符串拼接;仅在日志长度受限或框架堆栈干扰时裁剪,且不可删cause链。

Java中记录异常信息,关键不是“能不能记”,而是“记全不全、查不查得清”。直接用 logger.error("msg", e) 是最稳妥的起点,但多数人漏掉了上下文和堆栈裁剪逻辑。
为什么 logger.error(String, Throwable) 是首选写法
这是 SLF4J / Logback / Log4j2 等主流日志框架明确支持的重载方法,能自动提取 Throwable 的完整堆栈、cause 链、suppressed 异常(如 try-with-resources 中的多重异常),且不依赖 e.toString() 或 e.printStackTrace() 手动拼接。
常见错误是写成:logger.error("failed: " + e.getMessage(), e) —— 这既重复输出 message,又可能因 e.getMessage() 为 null 导致空指针;更糟的是写成 logger.error(e.toString()),完全丢失堆栈。
- ✅ 正确:
logger.error("User registration failed", e) - ❌ 错误:
logger.error("User registration failed: " + e.getMessage()) - ❌ 错误:
logger.error(e.getStackTrace().toString())
需要补充业务上下文时,别拼字符串
单纯记录异常不够,比如用户 ID、请求 ID、订单号这些关键字段必须和异常绑定,否则查问题时要来回翻日志。但不能靠字符串拼接,会污染日志格式、破坏结构化解析能力。
立即学习“Java免费学习笔记(深入)”;
推荐用 MDC(Mapped Diagnostic Context)注入上下文:
import org.slf4j.MDC;
MDC.put("userId", "U12345");
MDC.put("requestId", "req-abc789");
logger.error("Payment processing failed", e);
MDC.clear(); // 记得清理,尤其在非线程池场景下易泄漏配合 logback.xml 中配置 %X{userId} %X{requestId},就能让每条日志自动带上这些字段。
- MDC 是线程绑定的,适合 Web 请求生命周期(如 Filter 中 put/clear)
- 不要在异步线程中直接复用主线程 MDC,需显式拷贝:
MDC.getCopyOfContextMap() - 避免放入大对象或敏感信息(如明文密码、token)
什么时候该手动处理堆栈?
绝大多数场景不需要。但两类例外值得干预:
一是日志系统有单行长度限制(如某些 ELK 配置截断 4KB),深层嵌套异常可能被截断;二是堆栈里含大量无关框架代码(如 Spring AOP、CGLIB),掩盖真实出错点。
此时可用 Apache Commons Lang 的 ExceptionUtils.getRootCauseStackTrace(e) 或自定义裁剪逻辑,只保留业务包路径下的帧:
// 示例:只打印 com.myapp.service.* 和 com.myapp.controller.* 下的堆栈行
String relevantStack = Arrays.stream(e.getStackTrace())
.filter(frame -> frame.getClassName().startsWith("com.myapp."))
.map(StackTraceElement::toString)
.collect(Collectors.joining("\n"));- 裁剪前先确认是否真影响排查——很多团队根本没打开日志聚合的堆栈折叠功能
- 不要删掉 cause 链,
e.getCause()比堆栈裁剪更重要 - 生产环境禁用
e.printStackTrace(new PrintWriter(stringWriter)),性能差且不可控
真正难的不是记异常,而是确保每条错误日志都能在 3 分钟内定位到具体用户、具体操作、具体代码行。MDC 键名要不要加前缀、堆栈要不要过滤、error 日志要不要同步推送到告警通道——这些细节比“怎么调用 logger”重要得多。










