写在前面

经过本文将了解到什么是 MDC、MDC 应用中存在的问题、怎么处理存在的问题。

MDC 介绍

简介

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 供给的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当时线程绑定的哈希表,可以往其间增加键值对。MDC 中包括的内容可以被同一线程中履行的代码所访问。当时线程的子线程会继承其父线程中的 MDC 的内容。当需求记录日志时,只需求从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。关于一个 Web 应用来说,通常是在恳求被处理的最开端保存这些数据。

API 阐明
  • clear() => 移除一切 MDC

  • get (String key) => 获取当时线程 MDC 中指定 key 的值

  • getContext() => 获取当时线程 MDC 的 MDC

  • put(String key, Object o) => 往当时线程的 MDC 中存入指定的键值对

  • remove(String key) => 删去当时线程 MDC 中指定的键值对

优点

代码简练,日志风格统一,不需求在 log 打印中手动拼写 traceId,即

LOGGER.info("traceId:{} ", 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 中的键名称一致。

简单运用就这么简单,但是在有些状况下 traceId 将获取不到。

MDC 存在的问题

  • 子线程中打印日志丢掉 traceId

  • HTTP 调用丢掉 traceId

……丢掉traceId的状况,来一个再处理一个,绝不提早优化

处理 MDC 存在的问题

子线程日志打印丢掉 traceId

子线程在打印日志的过程中 traceId 将丢掉,处理办法为重写线程池。关于直接 new 创建线程的状况不考略,实际应用中应该避免这种用法。重写线程池无非是对任务进行一次封装。

线程池封装类:ThreadPoolExecutorMdcWrapper.java

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()));
    }
}

阐明:

  • 继承 ThreadPoolExecutor 类,重新履行任务的办法;

  • 经过 ThreadMdcUtil 对任务进行一次包装

线程 traceId 封装东西类:ThreadMdcUtil.java

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 办法。

代码等同于以下写法,会更直观。

public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
    return new Runnable() {
        @Override
        public void run() {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        }
    };
}

重新返回的是包装后的 Runnable,在该任务履行之前 runnable.run() 先将主线程的 Map 设置到当时线程中(即 MDC.setContextMap(context)),这姿态线程和主线程 MDC 对应的 Map 就是相同的了。

  • 判别当时线程对应 MDC 的 Map 是否存在,存在则设置;

  • 设置 MDC 中的 traceId 值,不存在则新生成。针对不是子线程的状况,如果是子线程,MDC 中 traceId 不为 null;

  • 履行 run 办法。

HTTP 调用丢掉 traceId

在运用 HTTP 调用第三方服务接口时 traceId 将丢掉,需求对 HTTP 调用东西进行改造。发送时,在 request header 中增加 traceId,在下层被调用方增加拦截器获取 header 中的 traceId 增加到 MDC 中。

HTTP 调用有多种办法,比较常见的有 HttpClient、OKHttp、RestTemplate,所以只给出这几种 HTTP 调用的处理办法。

HttpClient

完成 HttpClient 拦截器

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);
        }
    }
}

完成 HttpRequestInterceptor 接口并重写 process 办法。

如果调用线程中含有 traceId,则需求将获取到的 traceId 经过 request 中的 header 向下透传下去。

为 HttpClient 增加拦截器

private static CloseableHttpClient httpClient = HttpClientBuilder.create()
            .addInterceptorFirst(new HttpClientTraceIdInterceptor())
            .build();

经过 addInterceptorFirst 办法为 HttpClient 增加拦截器。

OKHttp

完成 OKHttp 拦截器

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;
    }
}

完成 Interceptor 拦截器,重写 interceptor 办法。完成逻辑和 HttpClient 差不多,如果可以获取到当时线程的 traceId 则向下透传。

为 OkHttp 增加拦截器

private static OkHttpClient client = new OkHttpClient.Builder()
          .addNetworkInterceptor(new OkHttpTraceIdInterceptor())
          .build();

调用 addNetworkInterceptor 办法增加拦截器。

RestTemplate

完成 RestTemplate 拦截器

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);
    }
}

完成 ClientHttpRequestInterceptor 接口,并重写 intercept 办法,其他逻辑都是相同的,这里不做重复阐明。

为 RestTemplate 增加拦截器

restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));

调用 setInterceptors 办法增加拦截器。

第三方服务拦截器

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 = TraceIdUtils.getTraceId();
        }
        MDC.put("traceId", 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 中获取t raceId;

  • 从 request header 中获取不到 traceId 则阐明不是第三方调用,直接生成一个新的 traceId;

  • 将生成的 traceId 存入 MDC 中。

除了需求增加拦截器之外,还需求在日志格局中增加 traceId 的打印,如下:

 <property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>

需求增加 %X{traceId}。

最终附:项目代码。

github.com/TiantianUpu…