SpringBoot项目中MDC使用介绍

一、概述

MDCMapped Diagnostic Context,映射诊断上下文)默认集成到spring-boot-starter-web项目中,只要引入spring-boot-starter-web依赖即可,可用于分布式链路追踪和日志关联。每个HTTP请求会自动分配唯一的traceId,追踪整个请求链路。

二、快速运用

1. 引入web依赖

<!--web依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2. 参数外部化配置

  • application.yml参数配置示例
mdc:
  enabled: true  # 启用MDC
  trace-id-header: X-Trace-Id  # 请求头名称
  include-trace-id-in-response: true  # 响应头返回traceId
  response-trace-id-header: X-Trace-Id # 响应头中traceId的名称
  • 自定义属性配置类示例
package com.yotexs.bt.enterprise.ledger.config;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * MDC配置属性类
 * 用于配置MDC相关的属性,如traceId请求头名称等
 * @author cloud
 * {@code @date} 2026-01-21
 */
@Getter
@Setter
@ConfigurationProperties(prefix = "mdc")
public class MdcProperties {

    /**
     * 是否启用MDC过滤器
     * 默认:true
     */
    private boolean enabled = true;

    /**
     * traceId请求头名称
     * 默认:X-Trace-Id
     */
    private String traceIdHeader = "X-Trace-Id";

    /**
     * 是否在响应头中返回traceId
     * 默认:true
     */
    private boolean includeTraceIdInResponse = true;

    /**
     * 响应头中traceId的名称
     * 默认:X-Trace-Id
     */
    private String responseTraceIdHeader = "X-Trace-Id";
}

3. 封装MDC工具类、过滤器

  • 封装MDC工具类示例
package com.yotexs.bt.enterprise.ledger.utils;

import org.slf4j.MDC;

import java.util.UUID;

/**
 * MDC工具类
 * 用于管理Mapped Diagnostic Context(映射诊断上下文)
 * 提供traceId的设置、获取和清理功能
 * @author cloud
 * {@code @date} 2026-01-21
 */
public class MdcUtils {

    /**
     * MDC中traceId的键名
     */
    public static final String TRACE_ID_KEY = "traceId";

    /**
     * 私有构造函数,防止实例化
     */
    private MdcUtils() {
    }

    /**
     * 生成新的traceId
     * 使用简化UUID(去除连字符),长度为32字符
     * @return traceId字符串
     */
    public static String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    /**
     * 设置traceId到MDC
     * @param traceId traceId字符串
     */
    public static void setTraceId(String traceId) {
        if (traceId != null && !traceId.isEmpty()) {
            MDC.put(TRACE_ID_KEY, traceId);
        }
    }

    /**
     * 获取当前线程的traceId
     * @return traceId字符串,如果不存在则返回null
     */
    public static String getTraceId() {
        return MDC.get(TRACE_ID_KEY);
    }

    /**
     * 从请求头获取或生成traceId并设置到MDC
     * 优先使用请求头中的traceId,如果不存在则生成新的
     * @param traceId 请求头中的traceId,可为null
     * @return 最终使用的traceId
     */
    public static String setTraceIdFromRequest(String traceId) {
        String finalTraceId = traceId;
        
        // 如果请求头中没有traceId,则生成新的
        if (finalTraceId == null || finalTraceId.isEmpty()) {
            finalTraceId = generateTraceId();
        }
        
        // 设置到MDC
        setTraceId(finalTraceId);
        
        return finalTraceId;
    }

    /**
     * 清理MDC中的traceId
     * 必须在请求处理完成后调用,防止线程池复用时数据污染
     */
    public static void clear() {
        MDC.remove(TRACE_ID_KEY);
    }

    /**
     * 清理MDC中所有键值对
     * 用于彻底清理MDC上下文
     */
    public static void clearAll() {
        MDC.clear();
    }
}
  • 添加拦截器,拦截所有请求,并给每一个请求设置或从请求头获取唯一的traceId,用于日志追踪;响应时按配置是否响应traceId,实现追踪延续
package com.yotexs.bt.enterprise.ledger.filter;

import com.yotexs.bt.enterprise.ledger.config.MdcProperties;
import com.yotexs.bt.enterprise.ledger.utils.MdcUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

/**
 * MDC过滤器
 * 负责为每个请求设置traceId,用于分布式链路追踪和日志关联
 * 优先级设置为最高(0),确保在所有其他过滤器之前执行
 * @author cloud
 * {@code @date} 2026-01-21
 */
@Component
public class MdcFilter extends OncePerRequestFilter {

    private final MdcProperties mdcProperties;

    public MdcFilter(MdcProperties mdcProperties) {
        this.mdcProperties = mdcProperties;
    }

    @Override
    protected boolean shouldNotFilter(@NonNull HttpServletRequest request) {
        // 如果MDC被禁用,则跳过此过滤器
        return !mdcProperties.isEnabled();
    }

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain)
            throws ServletException, IOException {

        try {
            // 1. 从请求头获取traceId,如果不存在则生成新的
            String traceId = request.getHeader(mdcProperties.getTraceIdHeader());
            traceId = MdcUtils.setTraceIdFromRequest(traceId);

            // 2. 如果配置了在响应头中返回traceId,则添加到响应头
            if (mdcProperties.isIncludeTraceIdInResponse()) {
                response.setHeader(mdcProperties.getResponseTraceIdHeader(), traceId);
            }

            // 3. 继续过滤器链
            filterChain.doFilter(request, response);

        } finally {
            // 4. 清理MDC,防止线程池复用时数据污染
            MdcUtils.clear();
        }
    }
}

4. logback-spring.xml配置

  • 日志格式中需确保日志格式中包含%X{traceId}:
    <!-- appender是configuration的子节点,是负责写日志的组件 -->
    <!-- ConsoleAppender:把日志输出到控制台 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 默认情况下,每个日志事件都会立即刷新到基础输出流。这种默认方法更安全,因为如果应用程序在没有正确关闭appender的情况下退出,则日志事件不会丢失 -->
        <!-- 但为了显着增加日志记录吞吐量,可以将immediateFlush属性设置为false -->
        <immediateFlush>true</immediateFlush>
        <encoder>
            <!--[TRACEID:%X{traceId}]:用于日志线程跟踪,结合MDC(映射诊断上下文)工具-->
            <!-- %37():如果字符没有37个字符长度,则左侧用空格补齐 -->
            <!-- %-37():如果字符没有37个字符长度,则右侧用空格补齐 -->
            <!-- %15.15():如果记录的线程字符长度小于15(第一个)则用空格在左侧补齐,如果字符长度大于15(第二个),则从开头开始截断多余的字符 -->
            <!-- %msg:日志打印详情 -->
            <!--%cyan():颜色转换词,用于在控制台中区分不同的日志部分-->
            <!--:%line:前面的:为分隔符,%line表示会输出日志代码所在的行号-->
            <!-- %-40.40():-表示左对齐(默认是右对齐)如果记录的logger字符长度小于40(第一个)则用空格在右侧补齐,如果字符长度大于40(第二个),则从开头开始截断多余的字符 -->
            <!-- %n:换行符 -->
            <!-- %highlight():转换说明符以粗体红色显示其级别为ERROR的事件,红色为WARN,BLUE为INFO,以及其他级别的默认颜色 -->
            <pattern>[TRACEID:%X{traceId}] %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level)-[%15.15(%thread)] %cyan(%-40.40(%logger{40}):%line): %msg%n</pattern>
            <!-- 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

5. 在代码中使用

@RestController
@Slf4j
public class UserController {
    
    @GetMapping("/user/{id}")
    public Result<User> getUser(@PathVariable Long id) {
        // 直接记录日志,traceId会自动包含
        log.info("查询用户,userId={}", id);
        User user = userService.getById(id);
        return Result.success(user);
    }
}

6. 查看日志

[TRACEID:abc123def456abc123def456abc123] 2026-01-22 15:00:00.000 INFO [http-nio-8080-exec-1] c.y.b.e.l.controller.UserController:25: 查询用户,userId=1

三、功能特性

  • 自动traceId生成:以UUID为例子,采用去除下划线的UUID,每个请求自动生成32字符的唯一traceId;
  • 请求头传递:支持从HTTP请求头获取traceId,便于跨服务追踪;
  • 响应头返回:自动在响应头中返回traceId;
  • 日志集成:所有日志自动包含traceId;
  • 线程安全:使用finally块确保MDC清理,防止线程池复用污染。

四、核心组件

1. MdcFilter过滤器

作用:拦截所有HTTP请求,为每个请求设置traceId

@Component
public class MdcFilter extends OncePerRequestFilter {
    
    private final MdcProperties mdcProperties;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain filterChain) {
        try {
            // 获取或生成traceId
            String traceId = MdcUtils.setTraceIdFromRequest(
                request.getHeader(mdcProperties.getTraceIdHeader())
            );
            
            // 设置响应头
            if (mdcProperties.isIncludeTraceIdInResponse()) {
                response.setHeader(mdcProperties.getResponseTraceIdHeader(), traceId);
            }
            
            filterChain.doFilter(request, response);
        } finally {
            MdcUtils.clear();  // 清理MDC(关键!)
        }
    }
}

2. MdcUtils工具类

public class MdcUtils {
    
    public static final String TRACE_ID_KEY = "traceId";
    
    // 生成新的traceId
    public static String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }
    
    // 获取当前线程的traceId
    public static String getTraceId() {
        return MDC.get(TRACE_ID_KEY);
    }
    
    // 从请求头获取或生成traceId
    public static String setTraceIdFromRequest(String traceId) {
        String finalTraceId = (traceId == null || traceId.isEmpty()) 
            ? generateTraceId() 
            : traceId;
        MDC.put(TRACE_ID_KEY, finalTraceId);
        return finalTraceId;
    }
    
    // 清理MDC
    public static void clear() {
        MDC.remove(TRACE_ID_KEY);
    }
}

3. 配置类

@Getter
@Setter
@ConfigurationProperties(prefix = "mdc")
public class MdcProperties {
    private boolean enabled = true;
    private String traceIdHeader = "X-Trace-Id";
    private boolean includeTraceIdInResponse = true;
    private String responseTraceIdHeader = "X-Trace-Id";
}

4. 过滤器配置类

@Configuration
public class FilterConfig {
    
    @Bean
    public FilterRegistrationBean<MdcFilter> mdcFilterRegistration() {
        FilterRegistrationBean<MdcFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(mdcFilter);
        registration.setName("mdcFilter");
        registration.setOrder(0);  // 最高优先级
        registration.addUrlPatterns("/*");
        return registration;
    }
}

五、完整示例展示

1. 完整示例:

  • Controller层:
@RestController
@RequestMapping("/api/demo")
@RequiredArgsConstructor
@Slf4j
public class DemoController {
    
    private final DemoService demoService;
    
    @GetMapping("/hello")
    public Result<String> hello() {
        log.info("收到hello请求");
        String result = demoService.processHello();
        log.info("处理完成,返回结果:{}", result);
        return Result.success(result);
    }
    
    @GetMapping("/user/{id}")
    public Result<User> getUser(@PathVariable Long id) {
        log.info("查询用户,userId={}", id);
        User user = demoService.getUserById(id);
        if (user == null) {
            log.warn("用户不存在,userId={}", id);
            return Result.error("用户不存在");
        }
        return Result.success(user);
    }
}
  • Service层:
@Service
@RequiredArgsConstructor
@Slf4j
public class DemoService {
    
    private final UserMapper userMapper;
    
    public String processHello() {
        log.info("Service层处理hello请求");
        return "Hello World";
    }
    
    public User getUserById(Long id) {
        log.info("开始查询数据库,userId={}", id);
        User user = userMapper.selectById(id);
        if (user != null) {
            log.info("数据库查询成功,userId={}", id);
        } else {
            log.warn("数据库中未找到用户,userId={}", id);
        }
        return user;
    }
}
  • 日志输出:
[TRACEID:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d] 2026-01-22 15:00:00.001 INFO [http-nio-8080-exec-1] c.y.b.e.l.controller.DemoController:25: 收到hello请求
[TRACEID:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d] 2026-01-22 15:00:00.002 INFO [http-nio-8080-exec-1] c.y.b.e.l.service.DemoService:18: Service层处理hello请求
[TRACEID:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d] 2026-01-22 15:00:00.003 INFO [http-nio-8080-exec-1] c.y.b.e.l.controller.DemoController:27: 处理完成,返回结果:Hello World
  • HTTP响应:
HTTP/1.1 200 OK
X-Trace-Id: a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d
Content-Type: application/json

{
  "code": 200,
  "message": "success",
  "data": "Hello World"
}

2. 获取traceId

// 获取当前请求的traceId
String traceId = MdcUtils.getTraceId();
log.info("当前请求的traceId: {}", traceId);

3. 异常处理中使用

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        String traceId = MdcUtils.getTraceId();
        log.error("业务异常,traceId={}, message={}", traceId, e.getMessage(), e);
        return Result.error(e.getMessage());
    }
}

六、常见问题

1. 日志中没有traceId

  • 原因:MDC过滤器未注册或配置错误;

  • 解决

    • 检查mdc.enabled=true

    • 检查FilterConfig配置;

    • 检查logback-spring.xml中是否有%X{traceId}占位符。

2. 异步任务中没有traceId

  • 原因:新线程未传递MDC上下文;

  • 解决:配置TaskDecorator。

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setTaskDecorator(new MdcTaskDecorator());
        executor.initialize();
        return executor;
    }
}

public class MdcTaskDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        Map<String, String> contextMap = MDC.getCopyOfContextMap();
        return () -> {
            try {
                if (contextMap != null) {
                    MDC.setContextMap(contextMap);
                }
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

3. 跨服务调用traceId丢失

  • 原因:未配置拦截器传递traceId;

  • 解决

    • RestTemplate:
@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setInterceptors(List.of((request, body, execution) -> {
            String traceId = MdcUtils.getTraceId();
            if (traceId != null) {
                request.getHeaders().set("X-Trace-Id", traceId);
            }
            return execution.execute(request, body);
        }));
        return restTemplate;
    }
}
  • Feign:
@Configuration
public class FeignConfig {
    @Bean
    public RequestInterceptor traceIdInterceptor() {
        return template -> {
            String traceId = MdcUtils.getTraceId();
            if (traceId != null && !traceId.isEmpty()) {
                template.header("X-Trace-Id", traceId);
            }
        };
    }
}

4. 定时任务中没有traceId

  • 原因:定时任务未生成traceId;

  • 解决

@Component
public class ScheduledTask {
    
    @Scheduled(cron = "0 0 2 * * ?")
    public void dailyTask() {
        String traceId = MdcUtils.generateTraceId();
        MdcUtils.setTraceId(traceId);
        
        try {
            log.info("开始执行定时任务");
            // 业务逻辑
        } finally {
            // 注意清理,否则会内存泄漏
            MdcUtils.clear();
        }
    }
}

5. traceId在不同请求中相同

  • 原因:线程池复用线程时,未清理MDC,导致traceId相同;

  • 解决

    • 确保所有线程池都配置了TaskDecorator;

    • 确保异步线程在finally块中清理MDC;

    • 检查是否有System.exit()导致finally跳过。

七、注意事项

1. 线程池复用问题

  • MDC是线程本地变量,使用线程池时必须清理:
// 正确做法
public void asyncMethod() {
    String traceId = MdcUtils.getTraceId();
    MdcUtils.setTraceId(traceId);
    try {
        // 业务逻辑
    } finally {
        MdcUtils.clear();  // 务必清理
    }
}

2. 异步任务传递traceId

  • 推荐使用TaskDecorator自动传递MDC上下文,避免手动操作;

3. 内存泄漏

  • 务必在finally块中清理MDC,防止内存泄漏;

4. 性能影响

  • MDC对性能影响很小,可以放心使用。避免频繁调用MdcUtils.getTraceId()

5. 日志检索

  • 通过traceId快速关联同一请求的所有日志:
grep "TRACEID:abc123def456abc123def456abc123" logs/info/info.log
posted @ 2026-01-22 17:38  flycloudy  阅读(9)  评论(0)    收藏  举报