【MDC】使用 MDC 封装一个 LogMdcUtil 工具类
- [【一】MDC 简介](#【一】MDC 简介)
- [【二】LogMdcUtil 工具类](#【二】LogMdcUtil 工具类)
- [【三】Spring Boot 集成:Web 请求拦截器](#【三】Spring Boot 集成:Web 请求拦截器)
-
- [【1】MDC 拦截器](#【1】MDC 拦截器)
- 【2】注册拦截器
- [【四】异步 / 线程池场景:MDC 上下文传递](#【四】异步 / 线程池场景:MDC 上下文传递)
-
- [【1】MdcTaskDecorator 线程装饰器](#【1】MdcTaskDecorator 线程装饰器)
- 【2】配置线程池使用该装饰器
- [【五】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 的全链路追踪方案)
【一】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。