logger.error("msg", e) 会丢关键参数,因Log4j/SLF4J该重载仅静态打印msg和单独堆栈,不关联上下文变量;应显式拼入orderId、userId等,用{}占位符并确保Throwable在最后。

为什么 logger.error("msg", e) 会丢掉关键参数
Java里最常见却最危险的日志写法,就是只记录异常本身,不带任何上下文。比如用户ID、订单号、请求体、SQL参数这些关键信息全在 e 外面,日志里只看到“NullPointerException”,根本没法定位是哪个用户、哪次调用出的问题。
根本原因在于:Log4j/SLF4J 的 logger.error(String, Throwable) 重载方法,**不会自动拼接前面的字符串和异常堆栈里的变量值**——它只把传入的 String 当作静态消息,Throwable 单独打印堆栈,二者之间没有关联。
- 错误示例:
logger.error("处理订单失败", ex);→ 日志里只有“处理订单失败”和一长串堆栈,看不到orderId或userId - 正确思路:把关键变量显式拼进日志消息里,让它们和异常出现在同一行或相邻行
- 别依赖 MDC(Mapped Diagnostic Context)自动注入——它容易漏设、线程污染、异步场景失效
怎么用 String.format 或模板占位符安全拼接参数
SLF4J 支持 {} 占位符,比字符串拼接更轻量、更安全(避免空指针、类型错位),也比 String.format 少一次格式化开销(SLF4J 在日志级别关闭时会跳过参数计算)。
- 推荐写法:
logger.error("订单处理失败,orderId={}, userId={}, status={}", orderId, userId, status, ex); - 注意顺序必须严格对应——第四个参数是
Throwable,且必须放在最后;否则会被当成普通参数,堆栈不显示 - 如果参数可能为
null,SLF4J 默认输出null字符串,不影响日志结构;不需要提前判空转成"null" - 避免嵌套调用:不要在日志参数里写
getOrderDetail().getItems().size(),万一中间某个方法抛 NPE,你就永远不知道原始orderId是多少
哪些参数绝对不能记,哪些必须记
不是所有变量都适合打到日志里。目标是「能快速还原现场」,而不是「把整个对象 dump 出来」。
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
立即学习“Java免费学习笔记(深入)”;
- 必须记的:
requestId(全链路追踪 ID)、userId、orderId、method(当前方法名)、httpStatus(如果是 Web 层) - 谨慎记的:
password、idCard、bankCard—— 即使脱敏也要评估是否真有必要;生产环境日志默认应禁用明文敏感字段 - 不该记的:
this、大集合(如userList.size() > 1000)、Base64 图片、完整 JSON body(用摘要或长度代替,如bodyLength={}) - 特别注意:数据库 SQL 中的
WHERE参数必须单独提取记录,不要只记sql字符串——因为预编译后它里面全是问号
异步线程、RPC 调用、Filter/Interceptor 里怎么保上下文
Spring 的 @Async、Dubbo 的 AsyncContext、WebFilter 的子线程,都会丢失主线程的局部变量和 MDC。这时候靠「传参」比靠「继承」更可靠。
- 在提交异步任务前,把关键 ID 提前捕获:
String requestId = MDC.get("requestId"); CompletableFuture.runAsync(() -> { MDC.put("requestId", requestId); doWork(); }); - Dubbo 消费端想透传参数,得用
RpcContext.getContext().setAttachment("traceId", ...),服务端再从 attachment 取,不能只靠 MDC - Filter 中记录入口参数后,建议立刻存到一个轻量对象(如
LogContext)并绑定到当前线程,后续 service 层直接取,避免层层透传 - 千万别在 finally 块里记日志——异常可能被上层吞掉,finally 执行时你已经不知道当初发生了什么
最麻烦的其实是跨系统调用时的上下文断连:HTTP header 传了 traceId,但下游没解析、没写进日志、没透传给它的下游……这种链路断裂,光靠日志格式解决不了,得靠规范+监控对齐。

















