一.MDC
1.MDC介绍
MDC(Mapped Diagnostic Context,映射调试上下文
)是 log4j 和 logback
提供的一种方便在多线程
场景下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始
保存这些数据。
-
简而言之,MDC就是日志框架提供的一个
InheritableThreadLocal
,所以它是线程安全的,在项目代码中可以将键值对放入其中,然后使用指定方式取出打印即可。 -
优点:代码简洁,日志风格统一,不需要在log打印中手动拼写traceId,即
log.info("traceId:{} ", traceId)
- 在 log4j 和 logback 的取值方式为:
%X{traceid}
2.API说明
-
clear():移除所有MDC
-
get (String key):获取当前线程MDC中指定key的值
-
getContext() : 获取当前线程MDC的MDC
-
put(String key, Object o) :往当前线程的MDC中存入指定的键值对
-
remove(String key) : 删除当前线程MDC中指定的键值对
-
getPropertyMap():返回当前线程的context map的直接引用!不是
拷贝副本
! -
getCopyOfContextMap():返回当前线程的context map的一个
副本
,对这个map的修改不会影响原来copyOnInheritThreadLocal
中的内容。
二.MDC使用
1.使用方式
public class Constants {
/**
* 日志跟踪id名。
*/
public static final String TRACE_ID= "trace_id";
/**
* 请求头跟踪id名。
*/
public static final String HTTP_HEADER_TRACE_ID = "app_trace_id";
}
public class TraceIdUtil {
public static String getTraceId(){
return UUID.randomUUID().toString().replace("-","");
}
}
HTTP调用第三方服务接口全流程traceId需要第三方服务配合,第三方服务需要添加拦截器拿到request header
中的traceId并添加到MDC中
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果有上层调用就用上层的ID
String traceId = request.getHeader(Constants.TRACE_ID);
if (traceId == null) {
traceId = TraceIdUtil.getTraceId();
}
MDC.put(Constants.TRACE_ID, 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 {
//调用结束后删除
MDC.remove(Constants.TRACE_ID);
}
}
修改日志格式
<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>
- 重点是
%X{traceId}
,traceId和MDC中的键名称一致
2.存在问题
2.1.子线程日志打印丢失traceId
- 子线程在打印日志的过程中traceId将丢失,当前线程创建的子线程获取不到ThreadLocal存储的键值
- 解决方式为
重写线程池
,对于直接new创建线程的情况不考略【实际应用中应该避免这种用法
】,重写线程池无非是对任务进行一次封装
- 解决方式为
问题重现:
@GetMapping("getUserByName")
public Result getUserByName(@RequestParam String name)
{
//主线程日志
logger.info("getUserByName paramter name:"+name);
for(int i=0;i<5;i++)
{
//子线程日志
threadPoolTaskExecutor.execute(()->{
logger.info("child thread:{}",name);
userService.getUserByName(name);
});
}
return Result.success();
}
运行结果:
2022-03-13 12:45:44.156 [http-nio-8089-exec-1] INFO [ec05a600ed1a4556934a3afa4883766a] c.s.fw.controller.UserController - getUserByName paramter name:1
2022-03-13 12:45:44.173 [Pool-A1] INFO [] c.s.fw.controller.UserController - child thread:1
线程traceId封装工具类
public class ThreadMdcUtil {
public static void setTraceIdIfAbsent() {
if (MDC.get(Constants.TRACE_ID) == null) {
MDC.put(Constants.TRACE_ID, TraceIdUtil.getTraceId());
}
}
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);
}
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}
说明【以封装Runnable为例】:
- 判断当前线程对应MDC的Map是否存在,存在则设置
- 设置MDC中的traceId值,不存在则新生成,针对不是子线程的情况,如果是子线程,MDC中traceId不为null
- 执行run方法
- 重新返回的是包装后的Runnable,在该任务执行之前【
runnable.run()
】先将主线程的Map设置到当前线程中【 即MDC.setContextMap(context)
】,这样子线程和主线程MDC对应的Map就是一样的了
因为Spring Boot ThreadPoolTaskExecutor 已经对ThreadPoolExecutor进行封装,只需要继承ThreadPoolTaskExecutor重写相关的执行方法即可。
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
@Override
public void execute(Runnable task) {
super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public <T> Future<T> submit(Runnable task, T result) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
}
@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()));
}
}
线程池配置
@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
2.2.HTTP调用丢失traceId
在使用HTTP调用第三方服务接口时traceId将丢失,需要对HTTP调用工具进行改造,在发送时在request header中添加traceId
,在下层被调用方添加拦截器获取header中的traceId
添加到MDC中
- HTTP调用有多种方式,比较常见的有
HttpClient、OKHttp、RestTemplate
,所以只给出这几种HTTP调用的解决方式
1.HttpClient
实现HttpRequestInterceptor
接口并重写process
方法
- 如果调用线程中含有
traceId
,则需要将获取到的traceId通过request中的header
向下透传下去
public class HttpClientTraceIdInterceptor implements HttpRequestInterceptor {
@Override
public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
String traceId = MDC.get(Constants.TRACE_ID);
//当前线程调用中有traceId,则将该traceId进行透传
if (traceId != null) {
//添加请求体
httpRequest.addHeader(Constants.TRACE_ID, traceId);
}
}
}
为HttpClient添加拦截器
private static CloseableHttpClient httpClient = HttpClientBuilder.create()
.addInterceptorFirst(new HttpClientTraceIdInterceptor())
.build();
2.OKHttp
– 实现Interceptor拦截器,重写interceptor方法,实现逻辑和HttpClient差不多,如果能够获取到当前线程的traceId则向下透传
public class OkHttpTraceIdInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
String traceId = MDC.get(Constants.TRACE_ID);
Request request = null;
if (traceId != null) {
//添加请求体
request = chain.request().newBuilder().addHeader(Constants.TRACE_ID, traceId).build();
}
Response originResponse = chain.proceed(request);
return originResponse;
}
}
为OkHttp添加拦截器
private static OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new OkHttpTraceIdInterceptor())
.build();
3. RestTemplate
实现ClientHttpRequestInterceptor接口,并重写intercept方法,其余逻辑都是一样的不重复说明
public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
String traceId = MDC.get(Constants.TRACE_ID);
if (traceId != null) {
httpRequest.getHeaders().add(Constants.TRACE_ID, traceId);
}
return clientHttpRequestExecution.execute(httpRequest, bytes);
}
}
为RestTemplate添加拦截器
restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));
2.3.第三方服务需要添加拦截器
需要第三方服务配合,添加拦截器拿到request header
中的traceId并添加到MDC中
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果有上层调用就用上层的ID
String traceId = request.getHeader(Constants.TRACE_ID);
if (traceId == null) {
traceId = TraceIdUtil.getTraceId();
}
MDC.put(Constants.TRACE_ID, 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 {
//调用结束后删除
MDC.remove(Constants.TRACE_ID);
}
}
- 先从request header中获取traceId
- 从request header中获取不到traceId则说明不是第三方调用,直接生成一个新的traceId
- 将生成的traceId存入MDC中