视频看了几百小时还迷糊?关注我,几分钟让你秒懂!
在前面三篇中,我们解决了异常处理、参数校验、响应结构三大基础问题。
但线上系统一旦出问题,你是不是经常听到:
“这个接口怎么又慢了?”
“用户说没收到订单,到底执行到哪一步了?”
“这个请求到底是谁调的?从哪来的?”
这时候,日志就成了你的“破案神器”!
今天我们就来聊聊:如何在 Spring Boot 中实现统一、高效、可追踪的日志记录。
一、需求场景
你负责一个支付系统,包含以下流程:
前端 → /api/pay → OrderService → PaymentService → 银行回调某天用户反馈:“付了钱但订单没变已支付”。
你需要快速回答:
- 这个请求的唯一标识是什么?
- 请求参数是什么?
- 调用了哪些服务?
- 每一步耗时多少?
- 最终卡在哪一步?
如果每个方法都手动写log.info("xxx"),不仅重复,而且无法关联整条链路!
二、解决方案:MDC + 拦截器 + 唯一 TraceId
✅ 正确做法(生产级推荐)
1. 什么是 MDC?
MDC(Mapped Diagnostic Context)是Logback / Log4j 提供的线程绑定的 Map,可以在线程上下文中存储键值对(如 traceId),并在日志模板中引用。
✅ 同一个请求的所有日志,自动带上相同的 traceId!
2. 定义拦截器生成 TraceId
// TraceIdInterceptor.java @Component public class TraceIdInterceptor implements HandlerInterceptor { private static final String TRACE_ID = "traceId"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 优先使用前端传入的 traceId(便于跨服务追踪) String traceId = request.getHeader(TRACE_ID); if (traceId == null || traceId.isEmpty()) { // 自动生成 UUID traceId = "T" + System.currentTimeMillis() + "-" + ThreadLocalRandom.current().nextInt(1000, 9999); } // 存入 MDC MDC.put(TRACE_ID, traceId); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 清理 MDC,防止内存泄漏(尤其在线程池中!) MDC.clear(); } }3. 注册拦截器
// WebConfig.java @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private TraceIdInterceptor traceIdInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(traceIdInterceptor).addPathPatterns("/api/**"); } }4. 配置 logback-spring.xml 输出 traceId
<!-- logback-spring.xml --> <configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <!-- 关键:通过 %X{traceId} 引用 MDC 中的值 --> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId}] %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> </root> </configuration>5. Controller & Service 中直接打日志
@RestController public class PayController { private static final Logger log = LoggerFactory.getLogger(PayController.class); @PostMapping("/pay") public CommonResult<String> pay(@RequestBody PayRequest request) { log.info("收到支付请求: {}", request); // 自动带 traceId! String orderId = orderService.createOrder(request); paymentService.execute(orderId); log.info("支付流程完成"); return CommonResult.success("支付成功"); } }日志输出示例:
2025-12-25 12:30:45.123 [http-nio-8080-exec-1] INFO [T1703490245123-5678] c.e.c.PayController - 收到支付请求: PayRequest(userId=1001, amount=99.9) 2025-12-25 12:30:45.200 [http-nio-8080-exec-1] INFO [T1703490245123-5678] c.e.s.OrderService - 创建订单成功,orderId=ORD20251225001 2025-12-25 12:30:45.350 [http-nio-8080-exec-1] INFO [T1703490245123-5678] c.e.s.PaymentService - 调用银行支付接口...✅ 所有日志通过T1703490245123-5678串联起来!
三、反例(千万别这么写!)
❌ 反例1:手动拼接 traceId
String traceId = generateTraceId(); log.info("[{}] 开始处理请求", traceId); log.info("[{}] 调用订单服务", traceId); log.info("[{}] 支付完成", traceId);问题:
- 代码冗余;
- 容易漏写;
- 多人协作时格式不统一。
❌ 反例2:忘记清理 MDC
// 错误:没有在 afterCompletion 中 MDC.clear()后果:
- 在 Tomcat 线程池中,MDC 会污染下一个请求!
- 出现“A 请求的日志里混着 B 请求的 traceId”这种诡异问题。
四、进阶:跨服务传递 TraceId(微服务必备)
如果你用 Feign 或 RestTemplate 调用其他服务,需要透传 traceId:
方式1:Feign 拦截器
@Component public class FeignTraceIdInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { String traceId = MDC.get("traceId"); if (traceId != null) { template.header("traceId", traceId); } } }方式2:RestTemplate 拦截器
@Bean public RestTemplate restTemplate() { RestTemplate rt = new RestTemplate(); rt.setInterceptors(Collections.singletonList((request, body, execution) -> { String traceId = MDC.get("traceId"); if (traceId != null) { request.getHeaders().add("traceId", traceId); } return execution.execute(request, body); })); return rt; }下游服务的拦截器会自动读取 header 中的
traceId,实现全链路追踪!
五、注意事项(面试高频!)
MDC 是线程绑定的!
- 如果你用
@Async、线程池、CompletableFuture,子线程拿不到 MDC! - 解决方案:使用
MDC.getCopyOfContextMap()手动传递。
- 如果你用
不要记录敏感信息
- 如密码、身份证、银行卡号,日志脱敏很重要!
日志级别合理使用
DEBUG:开发调试用,生产关闭;INFO:关键业务节点;WARN:可恢复的异常;ERROR:必须报警的严重错误。
结合 AOP 记录请求/响应(可选)
@Aspect @Component public class LogAspect { @Around("@annotation(org.springframework.web.bind.annotation.PostMapping)") public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable { // 记录入参、耗时等 } }注意:避免记录大对象(如文件流),防止 OOM。
六、总结
| 能力 | 价值 |
|---|---|
| ✅ 全链路追踪 | 快速定位问题发生位置 |
| ✅ 日志结构化 | 便于 ELK / Splunk 分析 |
| ✅ 降低排查成本 | 运维/开发都能看懂 |
| ✅ 提升系统可观测性 | 微服务架构的基石 |
掌握这套日志体系,你就能在故障复盘会上精准甩锅(划掉)精准定位!
视频看了几百小时还迷糊?关注我,几分钟让你秒懂!