<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Spring Boot + MDC 實現(xiàn)全鏈路調(diào)用日志跟蹤,這才叫優(yōu)雅!

          共 9536字,需瀏覽 20分鐘

           ·

          2022-02-26 17:41

          程序員的成長之路
          互聯(lián)網(wǎng)/程序員/技術(shù)/資料共享?
          關(guān)注


          閱讀本文大概需要 5.5?分鐘。

          作者:何甜甜在嗎
          來源:juejin.cn/post/6844904101483020295

          # 寫在前面


          通過本文將了解到什么是MDC、MDC應(yīng)用中存在的問題、如何解決存在的問題
          MDC介紹


          # 簡介:


          MDC(Mapped Diagnostic Context,映射調(diào)試上下文)是 log4j 、logback及l(fā)og4j2 提供的一種方便在多線程條件下記錄日志的功能。MDC 可以看成是一個與當(dāng)前線程綁定的哈希表,可以往其中添加鍵值對。


          MDC 中包含的內(nèi)容可以被同一線程中執(zhí)行的代碼所訪問。當(dāng)前線程的子線程會繼承其父線程中的 MDC 的內(nèi)容。當(dāng)需要記錄日志時,只需要從 MDC 中獲取所需的信息即可。MDC 的內(nèi)容則由程序在適當(dāng)?shù)臅r候保存進去。對于一個 Web 應(yīng)用來說,通常是在請求被處理的最開始保存這些數(shù)據(jù)


          # API說明:

          • clear() => 移除所有MDC

          • get (String key) => 獲取當(dāng)前線程MDC中指定key的值

          • getContext() => 獲取當(dāng)前線程MDC的MDC

          • put(String key, Object o) => 往當(dāng)前線程的MDC中存入指定的鍵值對

          • remove(String key) => 刪除當(dāng)前線程MDC中指定的鍵值對


          # 優(yōu)點:

          代碼簡潔,日志風(fēng)格統(tǒng)一,不需要在log打印中手動拼寫traceId,即LOGGER.info("traceId:{} ", traceId)

          # MDC使用

          1.添加攔截器
          public class LogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //如果有上層調(diào)用就用上層的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 { //調(diào)用結(jié)束后刪除 MDC.remove(Constants.TRACE_ID); }}

          2.修改日志格式
          "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調(diào)用丟失traceId


          ......丟失traceId的情況,來一個再解決一個,絕不提前優(yōu)化

          # 解決MDC存在的問題


          子線程日志打印丟失traceId


          子線程在打印日志的過程中traceId將丟失,解決方式為重寫線程池,對于直接new創(chuàng)建線程的情況不考略【實際應(yīng)用中應(yīng)該避免這種用法】,重寫線程池?zé)o非是對任務(wù)進行一次封裝

          線程池封裝類:ThreadPoolExecutorMdcWrapper.java
          public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,                                        BlockingQueue workQueue) {        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);????}    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,                                        BlockingQueue workQueue, ThreadFactory threadFactory) {        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);????}    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,                                        BlockingQueue workQueue, RejectedExecutionHandler handler) {        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);    }    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,                                        BlockingQueue 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  Future submit(Runnable task, T result) {        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);    }    @Override    public  Future submit(Callable task) {        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));    }    @Override    public Future submit(Runnable task) {        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));    }}

          說明:

          • 繼承ThreadPoolExecutor類,重新執(zhí)行任務(wù)的方法

          • 通過ThreadMdcUtil對任務(wù)進行一次包裝


          線程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 Callable wrap(final Callable callable, final Map 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 context) { return () -> { if (context == null) { MDC.clear(); } else { MDC.setContextMap(context); } setTraceIdIfAbsent(); try { runnable.run(); } finally { MDC.clear(); } }; }}

          說明【以封裝Runnable為例】:

          • 判斷當(dāng)前線程對應(yīng)MDC的Map是否存在,存在則設(shè)置

          • 設(shè)置MDC中的traceId值,不存在則新生成,針對不是子線程的情況,如果是子線程,MDC中traceId不為null

          • 執(zhí)行run方法


          代碼等同于以下寫法,會更直觀
          public static Runnable wrap(final Runnable runnable, final Map 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,在該任務(wù)執(zhí)行之前【runnable.run()】先將主線程的Map設(shè)置到當(dāng)前線程中【 即MDC.setContextMap(context)】,這樣子線程和主線程MDC對應(yīng)的Map就是一樣的了

          # HTTP調(diào)用丟失traceId


          在使用HTTP調(diào)用第三方服務(wù)接口時traceId將丟失,需要對HTTP調(diào)用工具進行改造,在發(fā)送時在request header中添加traceId,在下層被調(diào)用方添加攔截器獲取header中的traceId添加到MDC中


          HTTP調(diào)用有多種方式,比較常見的有HttpClient、OKHttp、RestTemplate,所以只給出這幾種HTTP調(diào)用的解決方式


          1.HttpClient:

          實現(xiàn)HttpClient攔截器
          public class HttpClientTraceIdInterceptor implements HttpRequestInterceptor { @Override public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException { String traceId = MDC.get(Constants.TRACE_ID); //當(dāng)前線程調(diào)用中有traceId,則將該traceId進行透傳 if (traceId != null) { //添加請求體 httpRequest.addHeader(Constants.TRACE_ID, traceId); } }}

          實現(xiàn)HttpRequestInterceptor接口并重寫process方法

          如果調(diào)用線程中含有traceId,則需要將獲取到的traceId通過request中的header向下透傳下去

          為HttpClient添加攔截器
          private static CloseableHttpClient httpClient = HttpClientBuilder.create() .addInterceptorFirst(new HttpClientTraceIdInterceptor()) .build();

          通過addInterceptorFirst方法為HttpClient添加攔截器

          2.OKHttp:

          實現(xiàn)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; }}


          實現(xiàn)Interceptor攔截器,重寫interceptor方法,實現(xiàn)邏輯和HttpClient差不多,如果能夠獲取到當(dāng)前線程的traceId則向下透傳

          為OkHttp添加攔截器
          private static OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor(new OkHttpTraceIdInterceptor()) .build();

          調(diào)用addNetworkInterceptor方法添加攔截器

          3.RestTemplate:

          實現(xiàn)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); }}

          實現(xiàn)ClientHttpRequestInterceptor接口,并重寫intercept方法,其余邏輯都是一樣的不重復(fù)說明

          為RestTemplate添加攔截器
          restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));

          調(diào)用setInterceptors方法添加攔截器

          4.第三方服務(wù)攔截器:


          HTTP調(diào)用第三方服務(wù)接口全流程traceId需要第三方服務(wù)配合,第三方服務(wù)需要添加攔截器拿到request header中的traceId并添加到MDC中
          public class LogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //如果有上層調(diào)用就用上層的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中獲取traceId

          • 從request header中獲取不到traceId則說明不是第三方調(diào)用,直接生成一個新的traceId

          • 將生成的traceId存入MDC中


          除了需要添加攔截器之外,還需要在日志格式中添加traceId的打印,如下:
          "pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>
          需要添加%X{traceId}

          推薦閱讀:

          int(1) 和 int(10) 有什么區(qū)別?資深開發(fā)竟然都理解錯了!

          肝了一晚上搞出來一個微信訂閱號鑒黃機器人

          互聯(lián)網(wǎng)初中高級大廠面試題(9個G)

          內(nèi)容包含Java基礎(chǔ)、JavaWeb、MySQL性能優(yōu)化、JVM、鎖、百萬并發(fā)、消息隊列、高性能緩存、反射、Spring全家桶原理、微服務(wù)、Zookeeper、數(shù)據(jù)結(jié)構(gòu)、限流熔斷降級......等技術(shù)棧!

          ?戳閱讀原文領(lǐng)取!? ? ? ? ? ? ? ??? ??? ? ? ? ? ? ? ? ? ?朕已閱?

          瀏覽 87
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  人妻.少妇.欧美.亚洲.日韩 | 看看操逼| 北条麻妃二区三区 | 亚洲第一av导航 亚洲福利精品内射 | 午夜试看120秒体验区的特点 |