应使用logger.error(string, throwable)记录完整异常栈,否则丢失类名、行号、嵌套原因及suppressed异常;异步场景需手动透传mdc,避免日志无上下文。

直接用 logger.error(String, Throwable) 记录完整异常栈
多数人只写 logger.error("出错了"),结果日志里只有文字,没有堆栈,根本没法定位问题。正确做法是把异常对象作为第二个参数传进去,SLF4J / Logback / Log4j2 都会自动打印完整堆栈跟踪。
常见错误现象:logger.error("查询用户失败: " + e.getMessage()) —— 这样只记了异常消息,丢了类名、行号、嵌套原因(Cause)、甚至 Suppressed 异常。
实操建议:
- 始终优先使用双参数形式:
logger.error("用户ID {} 查询失败", userId, e) - 不要手动调用
e.printStackTrace()或拼接e.toString(),那会丢失结构化信息 - 如果用了 Lombok 的
@Slf4j,它生成的log就是 SLF4J 的Logger,同样支持该用法
捕获异常后不要“吞掉”再空抛,否则日志断层
典型反模式:try { ... } catch (IOException e) { logger.error("读文件失败"); throw e; } —— 这里日志没带异常对象,上层再捕获时又可能重复记录,但关键上下文(比如当时处理的文件路径、用户ID)已经丢失。
立即学习“Java免费学习笔记(深入)”;
使用场景:服务中常见的“包装重抛”逻辑,比如把 SQLException 转成自定义 ServiceException。
实操建议:
- 重抛前务必记录原始异常:
logger.error("DB操作失败,SQL: {}", sql, originalEx) - 若要包装异常,用构造函数传入 cause:
throw new ServiceException("数据库繁忙", originalEx),确保栈信息可追溯 - 避免
catch后只写logger.error(...)然后return null或静默结束——这会让调用方误以为成功
在 finally 或 try-with-resources 中记录资源关闭异常要小心
Java 7+ 的 try-with-resources 会自动抑制(addSuppressed)关闭阶段抛出的异常。如果关闭时也出错,主异常的栈里会包含被抑制异常,但默认日志不会展开显示它们。
性能 / 兼容性影响:SLF4J 1.7.28+ 和 Logback 1.3+ 支持自动展开 Suppressed 异常,老版本则需手动遍历 e.getSuppressed() 输出。
实操建议:
- 升级日志框架到较新版本(如 Logback 1.4+),让
logger.error(..., e)自动打印 suppressed 异常 - 若无法升级,且怀疑关闭异常被忽略,可临时加一段手动输出:
for (Throwable s : e.getSuppressed()) { logger.warn("Suppressed: ", s); } - 不要在
finally块里无条件close()并吞异常——应检查资源是否为null,且对关闭异常做最小化记录(如仅 warn 级别)
异步线程或 Lambda 中抛异常,日志可能不进预期 Appender
比如用 CompletableFuture.supplyAsync(...) 或 Spring 的 @Async 方法,异常若未显式处理,会落到 ForkJoinPool.commonPool() 的默认异常处理器,最终可能只打印到 System.err,而不会触发你的 logback-spring.xml 配置。
常见错误现象:接口看似成功返回,但后台某异步任务失败了,日志里完全找不到痕迹。
实操建议:
- 所有异步逻辑必须有兜底异常处理:
whenComplete((r, e) -> { if (e != null) logger.error("异步发送通知失败", e); }) - Spring
@Async方法内部若可能抛异常,应在方法内 try-catch 并记录;或配置AsyncUncaughtExceptionHandler统一捕获 - 避免在 Lambda 中直接 throw 检查型异常(如
IOException),它无法通过函数式接口传播,容易导致编译错误或运行时UndeclaredThrowableException
最易被忽略的一点:日志框架的 error 方法虽能打印异常,但前提是线程上下文(如 MDC)已正确设置。异步场景下 MDC 不会自动继承,得手动 MDC.copyInto 或用 ThreadLocal 透传,否则日志里连 traceId 都没有,等于白记。










