【MDC】使用 MDC 封装一个 LogMdcUtil 工具类

【MDC】使用 MDC 封装一个 LogMdcUtil 工具类

  • [【一】MDC 简介](#【一】MDC 简介)
  • [【二】LogMdcUtil 工具类](#【二】LogMdcUtil 工具类)
  • [【三】Spring Boot 集成:Web 请求拦截器](#【三】Spring Boot 集成:Web 请求拦截器)
  • [【四】异步 / 线程池场景:MDC 上下文传递](#【四】异步 / 线程池场景:MDC 上下文传递)
  • [【五】logback-spring.xml 配置示例](#【五】logback-spring.xml 配置示例)
  • 【六】业务代码使用案例
  • [【七】Feign 调用透传 traceId](#【七】Feign 调用透传 traceId)
    • 【1】基础定义:常量与工具类
    • [【2】Web 服务入口过滤器(接收 / 生成 TraceId)](#【2】Web 服务入口过滤器(接收 / 生成 TraceId))
    • [【3】Feign 请求拦截器(透传 TraceId)](#【3】Feign 请求拦截器(透传 TraceId))
    • [【4】生产级方案:Spring Cloud Sleuth 开箱即用](#【4】生产级方案:Spring Cloud Sleuth 开箱即用)
  • [【八】基于 Spring Cloud Gateway 的全链路追踪方案](#【八】基于 Spring Cloud Gateway 的全链路追踪方案)
    • [【1】手动实现:Gateway 全局 TraceId 过滤器](#【1】手动实现:Gateway 全局 TraceId 过滤器)
    • [【2】生产级方案:Gateway + Sleuth + Zipkin](#【2】生产级方案:Gateway + Sleuth + Zipkin)

【一】MDC 简介

MDC(Mapped Diagnostic Context,映射诊断上下文)是 SLF4J 提供的线程级别的日志上下文容器,本质上是一个 ThreadLocal,可以在日志输出时自动注入上下文信息(如 traceId、userId、请求路径等),是分布式链路追踪和问题排查的常用手段。

【二】LogMdcUtil 工具类

java 复制代码
import org.slf4j.MDC;

import java.util.Map;
import java.util.UUID;

/**
 * MDC 日志上下文工具类
 * 用于在日志中注入链路追踪、用户标识等诊断信息
 */
public class LogMdcUtil {

    /** 链路追踪ID */
    public static final String TRACE_ID = "traceId";
    /** 用户ID */
    public static final String USER_ID = "userId";
    /** 请求URI */
    public static final String REQUEST_URI = "requestUri";
    /** 请求方法 */
    public static final String REQUEST_METHOD = "requestMethod";
    /** 客户端IP */
    public static final String CLIENT_IP = "clientIp";

    private LogMdcUtil() {
        throw new UnsupportedOperationException("Utility class cannot be instantiated");
    }

    // ==================== 核心操作 ====================

    /**
     * 生成并设置 traceId
     */
    public static String generateTraceId() {
        String traceId = UUID.randomUUID().toString().replace("-", "");
        MDC.put(TRACE_ID, traceId);
        return traceId;
    }

    /**
     * 设置指定 traceId
     */
    public static void setTraceId(String traceId) {
        MDC.put(TRACE_ID, traceId);
    }

    /**
     * 获取当前 traceId
     */
    public static String getTraceId() {
        return MDC.get(TRACE_ID);
    }

    /**
     * 存入键值对
     */
    public static void put(String key, String value) {
        if (key != null && value != null) {
            MDC.put(key, value);
        }
    }

    /**
     * 获取值
     */
    public static String get(String key) {
        return MDC.get(key);
    }

    /**
     * 移除指定键
     */
    public static void remove(String key) {
        MDC.remove(key);
    }

    // ==================== 批量操作 ====================

    /**
     * 批量存入上下文
     */
    public static void putAll(Map<String, String> contextMap) {
        if (contextMap != null && !contextMap.isEmpty()) {
            contextMap.forEach(MDC::put);
        }
    }

    /**
     * 获取当前线程全部上下文副本
     */
    public static Map<String, String> getCopyOfContextMap() {
        return MDC.getCopyOfContextMap();
    }

    /**
     * 设置整个上下文Map(用于线程池传递)
     */
    public static void setContextMap(Map<String, String> contextMap) {
        MDC.setContextMap(contextMap);
    }

    // ==================== 清理操作 ====================

    /**
     * 清空当前线程所有 MDC 内容
     * 务必在请求结束时调用,避免线程复用导致上下文污染
     */
    public static void clear() {
        MDC.clear();
    }

    /**
     * 仅清除 traceId
     */
    public static void removeTraceId() {
        MDC.remove(TRACE_ID);
    }
}

【三】Spring Boot 集成:Web 请求拦截器

通过拦截器在请求进入时生成 traceId,请求结束时清理 MDC,避免 Tomcat 线程池复用导致的上下文串扰。

【1】MDC 拦截器

java 复制代码
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class MdcInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 1. 生成链路ID(也可从上游请求头获取,实现全链路透传)
        String traceId = request.getHeader("X-Trace-Id");
        if (traceId == null || traceId.isEmpty()) {
            traceId = LogMdcUtil.generateTraceId();
        } else {
            LogMdcUtil.setTraceId(traceId);
        }

        // 2. 放入请求信息
        LogMdcUtil.put(LogMdcUtil.REQUEST_URI, request.getRequestURI());
        LogMdcUtil.put(LogMdcUtil.REQUEST_METHOD, request.getMethod());
        LogMdcUtil.put(LogMdcUtil.CLIENT_IP, getClientIp(request));

        // 3. 将 traceId 写入响应头,方便前端排查
        response.setHeader("X-Trace-Id", traceId);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) {
        // 请求结束必须清理,防止线程复用导致数据污染
        LogMdcUtil.clear();
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 多级代理时第一个IP为真实IP
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip;
    }
}

【2】注册拦截器

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private final MdcInterceptor mdcInterceptor;

    public WebMvcConfig(MdcInterceptor mdcInterceptor) {
        this.mdcInterceptor = mdcInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(mdcInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/error", "/static/**");
    }
}

【四】异步 / 线程池场景:MDC 上下文传递

MDC 基于 ThreadLocal,线程切换时会丢失。如果项目使用了 @Async 或自定义线程池,需要包装 Runnable/Callable 传递上下文。

【1】MdcTaskDecorator 线程装饰器

java 复制代码
import org.springframework.core.task.TaskDecorator;
import java.util.Map;

/**
 * 线程池任务装饰器,用于异步任务时传递 MDC 上下文
 */
public class MdcTaskDecorator implements TaskDecorator {

    @Override
    public Runnable decorate(Runnable runnable) {
        // 捕获父线程上下文
        Map<String, String> contextMap = LogMdcUtil.getCopyOfContextMap();

        return () -> {
            try {
                // 子线程设置上下文
                if (contextMap != null) {
                    LogMdcUtil.setContextMap(contextMap);
                }
                runnable.run();
            } finally {
                // 子线程执行完毕清理
                LogMdcUtil.clear();
            }
        };
    }
}

【2】配置线程池使用该装饰器

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
public class ThreadPoolConfig {

    @Bean("mdcExecutor")
    public Executor mdcExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("mdc-async-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 关键:设置任务装饰器,传递 MDC 上下文
        executor.setTaskDecorator(new MdcTaskDecorator());
        executor.initialize();
        return executor;
    }
}

【五】logback-spring.xml 配置示例

在日志格式中通过 %X{key} 引用 MDC 中的值:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <property name="LOG_PATTERN"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - [%X{traceId}] [%X{userId}] %msg%n"/>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

输出效果:

java 复制代码
2026-07-08 14:30:00.123 [http-nio-8080-exec-1] INFO  c.e.demo.controller.UserController - [a1b2c3d4e5f6] [1001] 查询用户信息成功

【六】业务代码使用案例

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    private static final Logger log = LoggerFactory.getLogger(UserController.class);

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        // 手动放入用户ID
        LogMdcUtil.put(LogMdcUtil.USER_ID, String.valueOf(id));
        
        log.info("开始查询用户信息");
        // ... 业务逻辑
        log.info("用户查询完成");
        
        return user;
    }
}

【七】Feign 调用透传 traceId

在 Feign 发起远程调用前,从当前线程的 MDC(日志映射上下文)或请求上下文中提取 TraceId,写入 Feign 请求头;下游服务通过 Web 过滤器读取请求头中的 TraceId,重新写入 MDC,实现跨服务链路 ID 透传,配合日志框架可实现全链路日志串联。

【1】基础定义:常量与工具类

java 复制代码
public interface TraceConstant {
    /** 链路追踪ID请求头 */
    String TRACE_ID = "X-Trace-Id";
    /** 日志MDC Key */
    String MDC_TRACE_ID = "traceId";
}
java 复制代码
import java.util.UUID;

public class TraceUtils {
    /** 生成全局唯一TraceId(生产环境建议用雪花算法) */
    public static String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }
}

【2】Web 服务入口过滤器(接收 / 生成 TraceId)

所有 Spring Boot Web 服务统一添加该过滤器,负责接收上游传递的 TraceId,无 TraceId 时生成并写入 MDC,请求结束后清理上下文。

java 复制代码
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
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;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 最高优先级,最先执行
public class TraceIdFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        try {
            // 1. 从请求头获取上游传递的TraceId,不存在则生成
            String traceId = request.getHeader(TraceConstant.TRACE_ID);
            if (!StringUtils.hasText(traceId)) {
                traceId = TraceUtils.generateTraceId();
            }

            // 2. 写入MDC,日志格式中可通过 %X{traceId} 打印
            MDC.put(TraceConstant.MDC_TRACE_ID, traceId);

            // 3. 响应头返回TraceId,方便前端/调用方排查问题
            response.setHeader(TraceConstant.TRACE_ID, traceId);

            filterChain.doFilter(request, response);
        } finally {
            // 4. 请求结束必须清理MDC,避免线程池复用导致上下文污染
            MDC.remove(TraceConstant.MDC_TRACE_ID);
        }
    }
}

【3】Feign 请求拦截器(透传 TraceId)

实现 Feign 原生的 RequestInterceptor 接口,在请求发送前将 MDC 中的 TraceId 写入请求头,传递给下游服务。

java 复制代码
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

@Configuration
public class FeignTraceConfig {

    @Bean
    public RequestInterceptor traceIdRequestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                // 从当前线程MDC获取TraceId
                String traceId = MDC.get(TraceConstant.MDC_TRACE_ID);
                if (StringUtils.hasText(traceId)) {
                    // 写入Feign请求头,下游服务通过TraceIdFilter接收
                    template.header(TraceConstant.TRACE_ID, traceId);
                }
            }
        };
    }
}

配置说明:

该配置类加 @Configuration 后,会对所有 Feign 客户端全局生效;

若只需部分 Feign 客户端生效,去掉 @Configuration,在 @FeignClient 注解的 configuration 属性中指定该类即可。

【4】生产级方案:Spring Cloud Sleuth 开箱即用

手动实现仅适合轻量场景,生产环境推荐直接使用 Spring Cloud Sleuth,它已自动实现 Feign、RestTemplate、消息队列等全组件的 TraceId 透传,无需手写拦截器。

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

日志格式配置(logback)

xml 复制代码
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - [traceId=%X{traceId}, spanId=%X{spanId}] - %msg%n</pattern>

自定义业务字段透传(可选)

如需透传租户 ID、用户 ID 等业务字段,配置 Baggage 即可自动全链路透传:

yml 复制代码
spring:
  sleuth:
    baggage:
      remote-fields: X-Tenant-Id, X-User-Id
      correlation-fields: X-Tenant-Id, X-User-Id

【八】基于 Spring Cloud Gateway 的全链路追踪方案

Gateway 作为流量入口,是全链路追踪的起点,负责生成全局 TraceId 并透传到所有下游服务,配合 Sleuth + Zipkin 可实现可视化链路监控。

方案架构

Gateway 入口:接收前端请求,生成 / 透传 TraceId,写入请求头转发给下游服务;

下游服务:通过上述 TraceIdFilter 或 Sleuth 接收 TraceId,继续在 Feign/DB/ 消息队列中透传;

日志采集:所有服务日志统一携带 TraceId,通过 ELK/Loki 等组件聚合检索;

链路可视化:整合 Zipkin/Jaeger 查看链路拓扑、耗时分布、异常定位。

【1】手动实现:Gateway 全局 TraceId 过滤器

Gateway 基于 WebFlux 响应式架构,线程切换频繁,MDC 原生 ThreadLocal 会失效,需配合 Reactor 上下文传递。

java 复制代码
import org.slf4j.MDC;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class TraceIdGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        // 1. 从请求头获取TraceId,不存在则生成
        String traceId = request.getHeaders().getFirst(TraceConstant.TRACE_ID);
        if (!StringUtils.hasText(traceId)) {
            traceId = TraceUtils.generateTraceId();
        }

        // 2. 构造新请求,将TraceId写入请求头,透传给下游服务
        ServerHttpRequest modifiedRequest = request.mutate()
                .header(TraceConstant.TRACE_ID, traceId)
                .build();

        // 3. 响应头返回TraceId给前端
        exchange.getResponse().getHeaders().add(TraceConstant.TRACE_ID, traceId);

        // 4. 当前线程写入MDC + 响应式上下文传递(解决线程切换MDC丢失)
        MDC.put(TraceConstant.MDC_TRACE_ID, traceId);
        return chain.filter(exchange.mutate().request(modifiedRequest).build())
                .doFinally(signal -> MDC.remove(TraceConstant.MDC_TRACE_ID));
    }

    @Override
    public int getOrder() {
        // 最高优先级,确保在所有过滤器之前执行
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

注意:手动实现仅能保证请求转发时透传 TraceId,Gateway 内部响应式日志的 MDC 仍会因线程切换丢失。生产环境不建议手动处理,直接使用 Sleuth 对 WebFlux 的原生支持。

【2】生产级方案:Gateway + Sleuth + Zipkin

(1)Gateway 服务引入依赖

xml 复制代码
<!-- Gateway 核心 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 链路追踪 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- Zipkin 数据上报 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>

(2)配置文件

yml 复制代码
spring:
  application:
    name: gateway-service
  zipkin:
    base-url: http://127.0.0.1:9411/  # Zipkin 服务地址
    sender:
      type: web  # 上报方式,生产环境推荐用kafka/rabbitmq异步上报
  sleuth:
    sampler:
      probability: 0.2  # 采样率,0-1之间,生产环境建议0.1-0.3,全量设为1.0
    gateway:
      enabled: true  # 开启Gateway链路自动埋点
    web:
      enabled: true

(3)下游服务接入

所有下游微服务只需引入相同的 Sleuth + Zipkin 依赖并配置相同的 Zipkin 地址,即可自动实现:

接收 Gateway 透传的 TraceId;

Feign、RestTemplate、MyBatis、Redis、MQ 等全组件自动埋点;

日志自动携带 traceId、spanId、parentSpanId。

相关推荐
神奇小汤圆1 小时前
Java 26正式发布!这3个新特性,让代码量直接减半
后端
再吃一根胡萝卜1 小时前
一亿数据下,如何实时统计“业务员业绩 TOP 10”?别再 GROUP BY 了
后端
plainGeekDev1 小时前
依赖管理 → Version Catalog
android·java·kotlin
Oneslide2 小时前
systemd-journald 使用与配置指南
后端
神奇小汤圆2 小时前
30分钟实现自己的AI聊天机器人(SpringBoot接入DeepSeek API)
后端
再吃一根胡萝卜2 小时前
从最左前缀到地址索引:我对 MySQL 索引的一次完整复盘
后端
郡杰2 小时前
Spring Boot 请求流转与 RESTful
后端
再吃一根胡萝卜2 小时前
Redis 都有哪些用处?都会用在什么地方?
后端
再吃一根胡萝卜3 小时前
分布式与分布式锁:从访问量到数据一致性
后端