⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 blog.csdn.net/qq_42910468 「一米阳光zw」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

前言

在 Spring Boot 项目中排查问题时,我们通常需要查看日志。但当请求量较大或者接口调用链路较长时,很难区分日志属于哪一次请求。

springcloud框架有现成的解决方案一般用的是Zipkin、OTel(兼容 Jaeger、Zipkin、Tempo),Springboot为了解决这个问题,我们可以给每个请求设置一个 traceId。

使用 traceId 时,可以通过某一条日志获取对应的 traceId,然后快速查询同一请求的所有日志,从而大大提高排查效率。

本文主要介绍如何在项目中使用 Spring 自带的 MDC(Mapped Diagnostic Context) 来实现 traceId,MDC不需要添加其他依赖,文章中处理了以下场景添加traceId:

  • HTTP 请求
  • MQ(RabbitMQ)
  • 线程池异步任务
  • 定时任务(xxljob)

MDC实现原理

MDC 是 SLF4J/Logback 提供的 线程级日志上下文存储。它内部通过 ThreadLocal<Map<String, String>> 保存上下文信息。

  • 当在某个线程里执行 MDC.put("traceId", "xxx") 时,traceId 会存入当前线程的 ThreadLocal 中。
  • 日志框架在输出日志时,会自动从 MDC 中获取 traceId 并填入日志模板。
  • 不同线程的 MDC 是独立的,每个线程都有自己的上下文,不会互相干扰。

logback文件的处理

控制台输出内容修改(开发环境)

需要在logback-spring.xml文件的property标签中日志输出模板中添加traceId的变量占位符,添加内容[traceId:%X{traceId}],CONSOLE_LOG_PATTERN属性中改变的是控制台的输出内容本地调试时候的输出

<property name="CONSOLE_LOG_PATTERN"
value="${DEFAULT_CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} [traceId:%X{traceId}] %clr(${LOG_LEVEL_PATTERN:-%5p})
%clr(${PID:- }){magenta} %clr(---){faint} %clr([%t]){faint} %clr(%-40.40logger{39}){cyan}
%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

根据自己的需要添加就行,我这里的格式是:[traceId:7e01902b18ef4c2b9f49609c57d769fa]

输出到文档中的修改(测试、正式环境)

修改logback-spring.xml的全量日志输出,包含了info与error级别的日志输出,需要在pattern标签下添加traceId项,"traceId": "%X{traceId}",这样的话像测试或者正式环境在收集日志到log文件中时也会加进去

Http请求处理

只需要添加一个过滤器设置traceId即可

import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;

/**
* 日志traceId功能
*
* @author: Czw
* @create: 2025-11-06 10:31
**/
@Component
public class TraceIdFilter extends OncePerRequestFilter {
private static final String TRACE_ID = "traceId";

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 跳过预检请求(OPTIONS)
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
filterChain.doFilter(request, response);
return;
}

try {
String traceId = UUID.randomUUID().toString().replace("-", "");
MDC.put(TRACE_ID, traceId);
filterChain.doFilter(request, response);
} finally {
MDC.remove(TRACE_ID);
}
}
}

线程池

需要单独处理Callable和Runnable,在外面包一层。由于线程池中的线程是复用的,所以在用完之后需要在finnally中清除设置的traceId,避免影响下一次任务

/**
* 异步执行
*
* @param task 任务
*/
public void execute(Runnable task) {
defaultThreadPoolExecutor.execute(wrap(task, MDC.getCopyOfContextMap()));
}

/**
* 提交一个有返回值的异步任务
*/
public <T> Future<T> submit(Callable<T> task) {
return defaultThreadPoolExecutor.submit(wrap(task, MDC.getCopyOfContextMap()));
}


* 封装 Runnable,复制 MDC 上下文
*/
private Runnable wrap(Runnable task, Map<String, String> contextMap) {
return () -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
if (contextMap != null) {
MDC.setContextMap(contextMap);
} else {
MDC.clear();
}
try {
task.run();
} finally {
// 恢复线程池线程原来的 MDC,避免影响下一次任务
if (previous != null) {
MDC.setContextMap(previous);
} else {
MDC.clear();
}
}
};
}

/**
* 封装 Callable,复制 MDC 上下文
*/
private <T> Callable<T> wrap(Callable<T> task, Map<String, String> contextMap) {
return () -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
if (contextMap != null) {
MDC.setContextMap(contextMap);
} else {
MDC.clear();
}
try {
return task.call();
} finally {
// 恢复线程池线程原来的 MDC,避免影响下一次任务
if (previous != null) {
MDC.setContextMap(previous);
} else {
MDC.clear();
}
}
};
}

mq(RabbitMq)

MQ的的话需要在sender时统一获取发送时的traceId,然后设置到mq的header中,然后利用Spring AMQP 提供了 RabbitListener 的 Advice 机制,可以对所有消费者统一处理,不需要在每一个consumer进行处理

消息生产者处理:

/**
* 同步发送mq (不管消费者有没有消费到,发出去消息就结束)
*
* @param typeEnum
* @param message
*/
public <T> void sendMq(MqEnum.TypeEnum typeEnum, MqMessage<T> message) {
rabbitTemplate.convertAndSend(MqEnum.Exchange.EXCHANGE_NAME, typeEnum.getRoutingKey(), message,
msg -> {
String traceId = MDC.get(TRACE_ID);
if (traceId == null) {
traceId = UUID.randomUUID().toString().replace("-", "");
MDC.put(TRACE_ID, traceId);
}
msg.getMessageProperties().getHeaders().put(TRACE_ID, traceId);
return msg;
});
}

利用Advice机制获取发送来的traceId然后设置到当前消费者的线程中

/**
* 透传MDC
* sendMq时设置MDC到header中,消费端
*
* @return {@link Advice }
* @author Czw
* @date 2025/11/06
*/
@Bean
public Advice traceIdAdvice() {
return (MethodInterceptor) invocation -> {
Object[] args = invocation.getArguments();
String traceId = null;

for (Object arg : args) {
if (arg instanceof Message message) {
traceId = (String) message.getMessageProperties().getHeaders().get(TRACE_ID);
break;
}
}

if (traceId != null) {
MDC.put(TRACE_ID, traceId);
}

try {
return invocation.proceed();
} finally {
MDC.remove(TRACE_ID);
}
};
}

/**
* 设置自定义的traceIdAdvice
*
* @param connectionFactory connectionFactory
* @param traceIdAdvice traceIdAdvice
* @return {@link SimpleRabbitListenerContainerFactory }
* @author Czw
* @date 2025/11/06
*/
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
ConnectionFactory connectionFactory,
Advice traceIdAdvice) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setAdviceChain(traceIdAdvice);
return factory;
}

定时任务(xxljob)

仅处理XXL-Job的定时任务,利用全局 AOP 切面自动加 traceId,避免每个定时任务都去加

/**
* @description:
* @author: Czw
* @create: 2025-11-07 15:17
**/
@Aspect
@Component
public class XxlJobTraceAspect {

@Pointcut("@annotation(com.xxl.job.core.handler.annotation.XxlJob)")
public void xxlJobMethods() {}

@Around("xxlJobMethods()")
public Object aroundXxlJob(ProceedingJoinPoint joinPoint) throws Throwable {
String traceId = UUID.randomUUID().toString().replace("-", "");
MDC.put(TRACE_ID, traceId);
try {
return joinPoint.proceed();
} finally {
MDC.remove(TRACE_ID);
}
}
private static final String TRACE_ID = "traceId";

}

效果

测试接口

@GetMapping(value = "/test/traceId/async")
public Result<NullResult> traceId() {
log.info("主 traceId");
asyncExecutors.execute(() -> log.info("execute traceId"));

asyncExecutors.submit(() -> {
log.info("submit traceId");
return "ok";
});

List<Runnable> list = new ArrayList<>();
list.add(() -> log.info("execute list traceId"));
asyncExecutors.execute(list);
return Result.buildSuccess();
}

@GetMapping(value = "/test/traceId/mq")
public Result<NullResult> mq() {
log.info("主mq traceId");
MqMessage<String> message = new MqMessage<>();
message.setData(JSON.toJSONString(Collections.emptyList()));
mqSender.sendMq(MqEnum.TypeEnum.PROP_SEND, message);
return Result.buildSuccess();
}

请求接口时

mq的效果,发送时的traceId与consumer中的traceId一致

线程池的效果,四种线程池的使用时发送方与内部的traceId一致

定时任务(xxljob)

文章目录
  1. 1. 前言
  2. 2. MDC实现原理
  3. 3. logback文件的处理
    1. 3.0.1. 输出到文档中的修改(测试、正式环境)
  • 4. Http请求处理
  • 5. 线程池
  • 6. mq(RabbitMq)
    1. 6.0.1. 消息生产者处理:
  • 7. 定时任务(xxljob)
  • 8. 效果