SpringAOP+Logback+MDC实现traceId全链路日志追踪

📅 2026/7/18 17:36:37 👁️ 阅读次数 📝 编程学习
SpringAOP+Logback+MDC实现traceId全链路日志追踪

这篇文章资料来自于网络,是对部分知识整理,这里只是记录一下,仅供参考

在分布式系统或微服务架构中,全链路追踪(Full-Chain Tracing)对诊断和监控系统的性能至关重要。这篇文章,我们将详细介绍如何使用 SLF4J 的 MDC 实现全链路的traceId

1. 什么是MDC?

MDC(Mapped Diagnostic Context,映射诊断上下文)是 SLF4J 提供的一种上下文机制,它允许在日志记录时附加一些关键的上下文信息(如traceIduserId等),这些信息可以在日志格式中被引用,从而丰富日志内容,便于后续的日志分析和追踪。

2. 如何实现全链路traceId

2.1 生成或提取traceId

在请求的入口点,通常来说是客户端(比如浏览器、移动应用等,也可以是 Web 控制器、服务网关等)生成一个唯一的traceId

import org.slf4j.MDC; import java.util.UUID; public void handleRequest(HttpServletRequest request) { String traceId = request.getHeader("X-Trace-Id"); if (traceId == null || traceId.isEmpty()) { traceId = UUID.randomUUID().toString(); } MDC.put("traceId", traceId); try { // 处理请求的业务逻辑 } finally { MDC.remove("traceId"); // 确保线程安全,避免内存泄漏 } }

2.2 配置日志格式以包含traceId

在日志配置文件(如logback.xmllog4j2.xml)中,将traceId添加到日志输出格式中。例如,使用 Logback 时:

<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [traceId:%X{traceId}] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="info"> <appender-ref ref="STDOUT" /> </root> </configuration>

这样,每条日志都会包含traceId,便于后续追踪。

2.3 传播traceId到下游服务

当一个服务调用下游服务时,需要确保traceId被传递。通常通过 HTTP 头部传递,例如使用X-Trace-Id或者tracestate头:

import org.springframework.web.client.RestTemplate; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpEntity; public void callDownstreamService() { String traceId = MDC.get("traceId"); HttpHeaders headers = new HttpHeaders(); headers.set("X-Trace-Id", traceId); HttpEntity<String> entity = new HttpEntity<>(headers); RestTemplate restTemplate = new RestTemplate(); restTemplate.exchange("http://downstream-service/api", HttpMethod.GET, entity, String.class); }

2.4 在下游服务中设置traceId到 MDC

下游服务在接收到请求时,需要从请求头中提取traceId并设置到 MDC:

public void handleDownstreamRequest(HttpServletRequest request) { String traceId = request.getHeader("X-Trace-Id"); if (traceId != null && !traceId.isEmpty()) { MDC.put("traceId", traceId); } else { traceId = UUID.randomUUID().toString(); MDC.put("traceId", traceId); } try { // 业务逻辑处理 } finally { MDC.remove("traceId"); } }

2.5 处理多线程环境下的traceId传播

在多线程环境中,如使用线程池或异步任务,MDC 的上下文默认不会自动传播到子线程。需要手动传递traceId

import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public void executeAsyncTask() { String traceId = MDC.get("traceId"); ExecutorService executor = Executors.newFixedThreadPool(10); executor.submit(() -> { MDC.put("traceId", traceId); try { // 异步任务逻辑 } finally { MDC.remove("traceId"); } }); }

为了简化多线程环境下的 MDC 传播,可以使用像 MDC Decorator 或自定义Executor来自动管理 MDC。

3. 使用拦截器或过滤器统一管理traceId

为了避免在每个请求处理方法中手动设置和清理traceId,可以使用 Spring 的拦截器(Interceptor)或 Servlet 过滤器(Filter)来统一管理:

示例:使用 Spring 的OncePerRequestFilter

import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.MDC; import org.springframework.web.filter.OncePerRequestFilter; public class TraceIdFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String traceId = request.getHeader("X-Trace-Id"); if (traceId == null || traceId.isEmpty()) { traceId = UUID.randomUUID().toString(); } MDC.put("traceId", traceId); try { filterChain.doFilter(request, response); } finally { MDC.remove("traceId"); } } }

将该过滤器注册到 Spring 容器中即可。

4. 示例代码

4.1 AOP切面例子

创建一个Spring AOP切面来拦截方法调用,并在方法调用前后设置和清除MDC中的traceId

@Aspect @Component public class TraceAspect { @Pointcut("execution(* com.yourpackage..*.*(..))") public void allMethods() {} @Before("allMethods()") public void beforeAdvice(JoinPoint joinPoint) { // 生成traceId,这里只是简单示例,实际应用中可能需要更复杂的逻辑 String traceId = UUID.randomUUID().toString(); MDC.put("traceId", traceId); } @After("allMethods()") public void afterAdvice() { // 清除MDC中的traceId MDC.clear(); } }

注意:上面的示例使用了@Before@After注解来分别在方法调用前和调用后设置和清除MDC中的traceId。但是,在真实的应用场景中,你可能希望在HTTP请求的开始和结束时设置和清除traceId,这时你可能需要使用@Around注解,并在AOP切面中处理HTTP请求。

4.2过滤器例子2

定义一个web全局拦截器,拦截HTTP请求中携带的traceId,如果没有携带就生成一个随机id,并在请求前后处理traceId

import org.slf4j.MDC; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Optional; import java.util.UUID; @Component public class MDCFilter implements Filter { private static final String REQUEST_ID_HEADER = "requestId"; private static final String REQUEST_ID = "requestId"; @Override public void init(FilterConfig filterConfig) { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String requestId = request.getHeader(REQUEST_ID_HEADER); String requestIdMDC = Optional.ofNullable(requestId).orElseGet(() -> UUID.randomUUID().toString()); response.setHeader(REQUEST_ID_HEADER, requestIdMDC); MDC.put(REQUEST_ID, requestIdMDC); try { filterChain.doFilter(request,response); } finally { MDC.clear(); } } @Override public void destroy() { } }

4.3 过滤器例子1

1. 过滤器类

import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.MDC; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.util.UUID; public class TraceIdFilter extends OncePerRequestFilter { private static final String TRACE_ID_HEADER = "X-Trace-Id"; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String traceId = request.getHeader(TRACE_ID_HEADER); if (traceId == null || traceId.isEmpty()) { traceId = UUID.randomUUID().toString(); } MDC.put("traceId", traceId); try { filterChain.doFilter(request, response); } finally { MDC.remove("traceId"); } } }

2. Spring Boot 中注册过滤器

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.boot.web.servlet.FilterRegistrationBean; @Configuration public class WebConfig { @Bean public FilterRegistrationBean<TraceIdFilter> traceIdFilter() { FilterRegistrationBean<TraceIdFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new TraceIdFilter()); registrationBean.addUrlPatterns("/*"); // 过滤所有请求 registrationBean.setOrder(1); // 优先级 return registrationBean; } }

3. 日志配置(logback.xml)

<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [traceId:%X{traceId}] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="info"> <appender-ref ref="STDOUT" /> </root> </configuration>

5 多线程情况

上述处理没有考虑多线程的场景,而MDC(Mapped Diagnostic Context)是基于线程的,它为每个线程维护一个独立的映射。这意味着,如果在不同的线程中处理相同的请求,并且你希望这些线程共享相同的trace ID,你需要确保正确地将trace ID从一个线程传递到另一个线程。

解决方式:采用TaskDecorator的线程任务装饰器方式为线程池的线程提供traceId的传递操作。

4.4.1 定义任务装饰器

import lombok.NonNull; import org.slf4j.MDC; import org.springframework.core.task.TaskDecorator; import org.springframework.util.ObjectUtils; import java.util.Map; public class MDCTaskDecorator implements TaskDecorator { @Override public @NonNull Runnable decorate(@NonNull Runnable runnable) { // 此时获取的是父线程的上下文数据 Map<String, String> contextMap = MDC.getCopyOfContextMap(); return () -> { try { if (!ObjectUtils.isEmpty(contextMap)) { MDC.setContextMap(contextMap); } runnable.run(); } finally { // 清除子线程的,避免内存溢出,就和ThreadLocal.remove()一个原因 MDC.clear(); } }; } }

4.4.2 定义线程池

@Configuration public class PoolConfig { @Bean("taskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //配置核心线程数 executor.setCorePoolSize(5); //配置最大线程数 executor.setMaxPoolSize(10); //配置队列大小 executor.setQueueCapacity(100); //配置线程池中的线程的名称前缀 executor.setThreadNamePrefix("mdc-trace-"); // 异步MDC executor.setTaskDecorator(new MDCTaskDecorator()); //执行初始化 executor.initialize(); return executor; } }

4.4.3 使用线程池

@Resource @Qualifier("taskExecutor") private Executor executor; @PostMapping("/pool") public String pool() throws Exception{ CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { log.info("线程池里面"); try{ Thread.sleep(1000); } catch (Exception ignored){} return ""; }, executor); future.thenApplyAsync( value ->{ log.info("线程池组合操作"); try{ Thread.sleep(1000); } catch (Exception ignored) {} return value + "1"; }, executor); return future.get(); }

或者异步调用时使用@Async("taskExecutor")。

6 其他MDC使用

拦截器

@Component public class LogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //如果有上层调用就用上层的ID String traceId = request.getHeader(TraceIdUtil.TRACE_ID); if (StringUtil.isEmpty(traceId)) { TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId()); } else { TraceIdUtil.setTraceId(traceId); } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //调用结束后删除 TraceIdUtil.remove(); } }

工具类

public class TraceIdUtil { public static final String TRACE_ID = "requestId"; public static String getTraceId() { String traceId =(String) MDC.get(TRACE_ID); return traceId == null ? "" : traceId; } public static void setTraceId(String traceId) { MDC.put(TRACE_ID,traceId); } public static void remove() { MDC.remove(TRACE_ID); } public static void clear() { MDC.clear(); } public static String generateTraceId() { return UUID.randomUUID().toString().replace("-", ""); }

日志文件配置

<property name="LOG_PATTERN" value="%date{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{requestId}] %logger{36} - %msg%n" />

重点是%X{requestId},requestId和MDC中的键名称保持一致。

MDC 存在的问题

至此基本的功能已经实现,但是存在一下几个问题

  • 多线程情况下,子线程中打印日志会丢失traceId.
  • HTTP跨服务之间的调用丢失traceId.

子线程日志打印丢失traceId

解决方案

子线程在打印日志的过程中traceId将丢失,解决方案为重写线程池(对于直接new Thread 创建线程的情况不考略),实际开发中也需要禁止这种情况。

public class ThreadPoolExecutorMdcWrapper extends ThreadPoolTaskExecutor { private static final long serialVersionUID = 3940722618853093830L; @Override public void execute(Runnable task) { super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap())); } @Override public <T> Future<T> submit(Callable<T> task) { return super .submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap())); } @Override public Future<?> submit(Runnable task) { return super .submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap())); } }

因为Spring Boot ThreadPoolTaskExecutor 已经对ThreadPoolExecutor进行封装,只需要继承ThreadPoolTaskExecutor重写相关的执行方法即可。

public class ThreadMdcUtil { public static void setTraceIdIfAbsent() { if (MDC.get(TraceIdUtil.TRACE_ID) == null) { MDC.put(TraceIdUtil.TRACE_ID, TraceIdUtil.generateTraceId()); } } public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) { return () -> { if (context == null) { MDC.clear(); } else { MDC.setContextMap(context); } setTraceIdIfAbsent(); try { return callable.call(); } finally { MDC.clear(); } }; } public static Runnable wrap(final Runnable runnable, final Map<String, String> context) { return () -> { if (context == null) { MDC.clear(); } else { MDC.setContextMap(context); } //设置traceId setTraceIdIfAbsent(); try { runnable.run(); } finally { MDC.clear(); } }; } }

代码说明:

  • 判断当前线程对应MDC的Map是否存在,如果存在则设置
  • 设置MDC中的traceId值,不存在则新生成,如果是子线程,MDC中traceId不为null
  • 执行run方法

线程池配置

@Configuration public class ThreadPoolTaskExecutorConfig { //最大可用的CPU核数 public static final int PROCESSORS = Runtime.getRuntime().availableProcessors(); @Bean public ThreadPoolExecutorMdcWrapper getExecutor() { ThreadPoolExecutorMdcWrapper executor =new ThreadPoolExecutorMdcWrapper(); executor.setCorePoolSize(PROCESSORS *2); executor.setMaxPoolSize(PROCESSORS * 4); executor.setQueueCapacity(50); executor.setKeepAliveSeconds(60); executor.setThreadNamePrefix("Task-A"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); executor.initialize(); return executor; } }

重新运行结果发现子线程能够正常获取traceid信息进行跟踪。

2022-03-13 13:19:30.688 [Task-A1] INFO [482929425cbc4476a4e7168615af7890] c.s.fw.controller.UserController - child thread:1 2022-03-13 13:19:31.003 [Task-A1] INFO [482929425cbc4476a4e7168615af7890] c.s.fw.service.impl.UserServiceImpl - name:1

HTTP调用丢失traceId

HTTP调用第三方服务接口时traceId丢失,需要在发送请求时在Request Header中添加traceId,在被调用方添加拦截器获取header中的traceId添加到MDC中。

HTTP调用有多种方式,比较常见的有HttpClient、OKHttp、RestTemplate,以RestTemplate调用为例。

接口调用方
public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { String traceId=MDC.get("requestId"); if(traceId!=null) { request.getHeaders().set("requestId", traceId); } else { request.getHeaders().set("requestId", UUID.randomUUID().toString().replace("-", "")); } return clientHttpRequestExecution.execute(request, body); } }

RestTemplate添加拦截器即可。

restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));

2.第三方服务需要添加拦截器。
@Component public class LogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //如果有上层调用就用上层的ID String traceId = request.getHeader(TraceIdUtil.TRACE_ID); if (StringUtil.isEmpty(traceId)) { TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId()); } else { TraceIdUtil.setTraceId(traceId); } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //调用结束后删除 TraceIdUtil.remove(); } }

其他HttpClient、OKHttp的实现方式与RestTemplate基本相同,这里就不一一列举。

7 ThreadContext介绍

ThreadContext.put是 Log4j2 中 用于在高并发或多线程场景下追踪请求的核心工具 。它允许你在当前线程中绑定键值对(如userIdrequestId),并将这些上下文信息自动追加到日志输出中。以下是具体的实现步骤与使用规范:

1. 核心使用步骤

在 Java 代码的业务入口(如 Web 拦截器或方法开头)调用.put存入数据,并在日志输出后手动清理:

import org.apache.logging.log4j.ThreadContext; // 1. 存入上下文信息 ThreadContext.put("userId", "123456"); ThreadContext.put("requestId", "req-abc-999"); logger.info("用户登录成功"); // 2. 【务必执行】请求结束时清理上下文,防止线程复用导致数据污染 ThreadContext.clearAll();

请谨慎使用此类代码。

2. 日志配置文件修改(PatternLayout)

要将存入的上下文变量打印出来,需在 Log4j2 配置文件中使用%X占位符: [1, 2, 3]

  • 打印指定变量%X{userId}
  • 打印所有变量%X

配置示例

<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level [%X{userId}] [%X{requestId}] - %msg%n" />

请谨慎使用此类代码。

3. 三大避坑指南

  • 防止内存泄漏与上下文污染:由于现代服务器普遍使用线程池(线程复用),如果不调用ThreadContext.clearAll()ThreadContext.remove("key"),后续分配到该线程的其他请求可能会打印出旧请求的数据。
  • Web 请求拦截建议:在微服务或 Web 应用中,推荐在请求拦截器(Interceptor)的preHandleput,并在afterCompletionclearAll
  • 子线程传递:默认的ThreadContext在主子线程间无法共享。如需在异步或多线程间传递上下文,需使用log4j2.component.properties开启对应的继承机制

8. 注意事项

在使用一项技术或者一个框架时,我们都需要知道有什么弊端,这样才能更好的帮助我们做好技术决策,下面是使用MDC需要注意的事项:

  1. 线程安全:在多线程环境下,确保MDC的设置和清理,以避免traceId泄漏到其他线程。
  2. 性能影响:频繁操作MDC可能略微影响性能,需权衡日志详细程度和性能需求。
  3. 日志存储:确保日志存储系统支持和索引traceId,以便高效查询和分析。
  4. 异常处理:在异常情况下,确保traceId仍能正确记录在日志中,有助于快速定位问题。

9. 总结

本文,我们分析了如何使用MDC实现全链路追踪,通过合理使用 SLF4J的MDC,可以在分布式系统中实现全链路的traceId追踪,提升日志的可读性和追踪能力。但是,对于更复杂的需求,我们还是建议结合专门的分布式追踪工具,比如 Spring Cloud Sleuth、Zipkin 或 Jaeger,以获得更全面的追踪能力。

10 参考

https://juejin.cn/post/7074461710030995492

https://juejin.cn/post/7384740010896424979

https://juejin.cn/post/7461206556317237286