MDC是一种日志记录技术,它允许我们将上下文信息存储在线程局部变量中,并在整个线程执行期间共享这些信息。这些上下文信息可以是任何东西,例如请求ID、用户ID、会话ID等等。MDC允许我们在跨多个线程和组件的日志记录中,轻松地将所有相关日志记录关联起来。
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。
当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。
org.slf4j.MDC的API:
clear() :移除所有MDC
get (String key) :获取当前线程MDC中指定key的值
getContext() :获取当前线程MDC的MDC
put(String key, Object o) :往当前线程的MDC中存入指定的键值对
remove(String key) :删除当前线程MDC中指定的键值对
import org.slf4j.MDC;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
public final class ThreadMdcUtil {
private static final String TRACE_ID = "TRACE_ID";
public static String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "");
}
public static void setTraceIdIfAbsent() {
if (MDC.get(TRACE_ID) == null) {
MDC.put(TRACE_ID, generateTraceId());
}
}
public static void remove() {
MDC.remove(TRACE_ID);
}
/**
* 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
*
* @param callable
* @param context
* @param
* @return
*/
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();
}
};
}
/**
* 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
*
* @param runnable
* @param context
* @return
*/
public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}
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;
public class LogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
ThreadMdcUtil.setTraceIdIfAbsent();
try {
filterChain.doFilter(request, response);
} finally {
ThreadMdcUtil.remove();
}
}
}
配置bean:
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(Integer.MIN_VALUE);
return filterRegistrationBean;
}
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class LogInterceptor implements HandlerInterceptor {
private static final String TRACE_ID = "TRACE_ID";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
ThreadMdcUtil.setTraceIdIfAbsent();
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) {
ThreadMdcUtil.remove();
}
}
配置:
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor).addPathPatterns("/**");
}
在Spring Boot中,我们可以使用MDC Filter来轻松地将MDC集成到我们的应用程序中。MDC Filter是一个Servlet过滤器,它允许我们在每个请求的开始时将上下文信息存储在MDC中,并在整个请求处理期间共享这些信息。在请求处理结束时,MDC Filter还会清除MDC中的上下文信息。
以下是一个简单的示例,展示了如何使用MDC Filter将请求ID存储在MDC中:
public class RequestIdMdcFilter implements Filter {
private static final String REQUEST_ID_MDC_KEY = "requestId";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
try {
String requestId = UUID.randomUUID().toString();
MDC.put(REQUEST_ID_MDC_KEY, requestId);
chain.doFilter(request, response);
} finally {
MDC.remove(REQUEST_ID_MDC_KEY);
}
}
}
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ScheduledTaskAspect {
@Around("@annotation(org.springframework.scheduling.annotation.Scheduled)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 设置MDC上下文信息
ThreadMdcUtil.setTraceIdIfAbsent();
try {
// 执行定时任务方法
return joinPoint.proceed();
} finally {
ThreadMdcUtil.remove();
}
}
}
首先创建个@RabbitListener的切面,在执行完成之后删掉TRACE_ID
import lombok.extern.java.Log;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
@Log
public class RabbitMqListenerAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object returnValue = methodInvocation.proceed();
ThreadMdcUtil.remove();
return returnValue;
}
}
然后写个配置类:
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Slf4j
public class RabbitMQConfig {
private static final String TRACE_ID = "TRACE_ID";
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
rabbitTemplate.setBeforePublishPostProcessors(message -> {
// 在发送消息之前执行的逻辑
message.getMessageProperties().setHeader(TRACE_ID, MDC.get(TRACE_ID));
return message;
});
return rabbitTemplate;
}
@Bean(name = "rabbitListenerContainerFactory")
public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(
SimpleRabbitListenerContainerFactoryConfigurer configurer, ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setAfterReceivePostProcessors(message -> {
MDC.put(TRACE_ID, message.getMessageProperties().getHeader(TRACE_ID));
return message;
});
factory.setAdviceChain(new RabbitMqListenerAdvice());
configurer.configure(factory, connectionFactory);
return factory;
}
}
一旦我们将MDC集成到我们的应用程序中,就可以在日志记录中使用它了。以下是一个简单的示例:
使用lombok注解@Slf4j,然后在类中使用log.info()就可以了
在本文中,我们介绍了如何使用Spring Boot MDC Filter来实现请求跟踪和日志记录。通过使用MDC Filter,我们可以轻松地将MDC集成到我们的应用程序中,并在跨多个线程和组件的日志记录中轻松地将所有相关日志记录关联起来。如果您正在开发分布式系统,并且需要一种方式来跟踪请求并将所有相关日志记录关联起来,那么MDC是一个值得考虑的选择。