Java高级工程师面试题详解(四):Spring Boot 中如何实现统一日志记录与链路追踪?
2026/7/30 19:19:07 网站建设 项目流程

视频看了几百小时还迷糊?关注我,几分钟让你秒懂!

在前面三篇中,我们解决了异常处理、参数校验、响应结构三大基础问题。
但线上系统一旦出问题,你是不是经常听到:

“这个接口怎么又慢了?”
“用户说没收到订单,到底执行到哪一步了?”
“这个请求到底是谁调的?从哪来的?”

这时候,日志就成了你的“破案神器”!

今天我们就来聊聊:如何在 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,实现全链路追踪!


五、注意事项(面试高频!)

  1. MDC 是线程绑定的!

    • 如果你用@Async、线程池、CompletableFuture,子线程拿不到 MDC
    • 解决方案:使用MDC.getCopyOfContextMap()手动传递。
  2. 不要记录敏感信息

    • 如密码、身份证、银行卡号,日志脱敏很重要!
  3. 日志级别合理使用

    • DEBUG:开发调试用,生产关闭;
    • INFO:关键业务节点;
    • WARN:可恢复的异常;
    • ERROR:必须报警的严重错误。
  4. 结合 AOP 记录请求/响应(可选)

@Aspect @Component public class LogAspect { @Around("@annotation(org.springframework.web.bind.annotation.PostMapping)") public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable { // 记录入参、耗时等 } }

注意:避免记录大对象(如文件流),防止 OOM。


六、总结

能力价值
✅ 全链路追踪快速定位问题发生位置
✅ 日志结构化便于 ELK / Splunk 分析
✅ 降低排查成本运维/开发都能看懂
✅ 提升系统可观测性微服务架构的基石

掌握这套日志体系,你就能在故障复盘会上精准甩锅(划掉)精准定位


视频看了几百小时还迷糊?关注我,几分钟让你秒懂!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询