日志之MDC和異步多線程間傳遞線程ID

日志追蹤對于接口故障排查非常重要,可以有效、快捷的定位故障點,但在多線程環(huán)境中,若沒有相關框架的支持,想要實現(xiàn)日志追蹤,就需要編碼實現(xiàn)將主線程的日志參數(shù)傳遞給子線程,本文就在線程池場景下借助MDC實現(xiàn)了traceId參數(shù)的透傳

1 MDC

1.1 簡介

MDCMapped Diagnostic Context,映射調試上下文)是 log4jlogback 提供的一種方便在多線程條件下記錄日志的功能。某些應用程序采用多線程的方式來處理多個用戶的請求。在一個用戶的使用過程中,可能有多個不同的線程來進行處理。典型的例子是Web 應用服務器。當用戶訪問某個頁面時,應用服務器可能會創(chuàng)建一個新的線程來處理該請求,也可能從線程池中復用已有的線程。在一個用戶的會話存續(xù)期間,可能有多個線程處理過該用戶的請求。這使得比較難以區(qū)分不同用戶所對應的日志。當需要追蹤某個用戶在系統(tǒng)中的相關日志記錄時,就會變得很麻煩。

一種解決的辦法是采用自定義的日志格式,把用戶的信息采用某種方式編碼在日志記錄中。這種方式的問題在于要求在每個使用日志記錄器的類中,都可以訪問到用戶相關的信息。這樣才可能在記錄日志時使用。這樣的條件通常是比較難以滿足的。MDC 的作用是解決這個問題。MDC 可以看成是一個與當前線程綁定的哈希表,可以往其中添加鍵值對。MDC 中包含的內(nèi)容可以被同一線程中執(zhí)行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內(nèi)容。當需要記錄日志時,只需要從 MDC 中獲取所需的信息即可。MDC 的內(nèi)容則由程序在適當?shù)臅r候保存進去。對于一個 Web 應用來說,通常是在請求被處理的最開始保存這些數(shù)據(jù)

1.2 MDC坐標和使用

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
  </dependency>
  <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.7.21</version>
  </dependency>

log4j.xml配置樣例,追蹤日志自定義格式主要在name="traceId"layout里面進行設置,我們使用%X{traceId}來定義此處會打印MDC里面keytraceIdvalue,如果所定義的字段在MDC不存在對應的key,那么將不會打印,會留一個占位符
點擊了解Loback.xml文件解釋

1.3 MDC實現(xiàn)原理

MDCSLF4J/Logback 提供的 線程級日志上下文存儲。它內(nèi)部通過 ThreadLocal<Map<String, String>> 保存上下文信息。

  • 當在某個線程里執(zhí)行 MDC.put("traceId", "xxx") 時,traceId 會存入當前線程的 ThreadLocal 中。
  • 日志框架在輸出日志時,會自動從 MDC 中獲取 traceId 并填入日志模板。
  • 不同線程的 MDC 是獨立的,每個線程都有自己的上下文,不會互相干擾。

1.4 主要方法

API 說明:

  • clear():移除所有MDC
  • get (String key):獲取當前線程 MDC 中指定 key 的值
  • getCopyOfContextMap():將MDC從內(nèi)存獲取出來,再傳給線程
  • put(String key, Object o):往當前線程的 MDC 中存入指定的鍵值對
  • remove(String key):刪除當前線程 MDC 中指定的鍵值對
  • setContextMap():將父線程的MDC內(nèi)容傳給子線程

MDC異步線程間傳遞:
MDC的put時,子線程在創(chuàng)建的時候會把父線程中的inheritableThreadLocals變量設置到子線程的inheritableThreadLocals中,而MDC內(nèi)部是用InheritableThreadLocal實現(xiàn)的,所以會把父線程中的上下文帶到子線程中
但在線程池中,由于線程會被重用,但是線程本身只會初始化一次,所以之后重用線程的時候,就不會進行初始化操作了,也就不會有父線程inheritableThreadLocals拷貝到子線程中的過程了,這個時候如果還想傳遞父線程的上下文的話,就要使用getCopyOfContextMap方法

2 多線程間使用

2.1 MDC工具類

定義MDC工具類,支持RunnableCallable兩種,目的就是為了把父線程的traceId設置給子線程

import org.slf4j.MDC;
import org.springframework.util.CollectionUtils;

import java.util.Map;
import java.util.concurrent.Callable;

/**
 * @Description 封裝MDC用于向線程池傳遞
 */
public class MDCUtil {
    // 設置MDC中的traceId值,不存在則新生成,針對不是子線程的情況,
    // 如果是子線程,MDC中traceId不為null
    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 (CollectionUtils.isEmpty(context)) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                return callable.call();
            } finally {//清除子線程的,避免內(nèi)存溢出,就和ThreadLocal.remove()一個原因
                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();
            }
        };
    }

    public static void setMDCContextMap(final Map<String, String> context) {
        if (CollectionUtils.isEmpty(context)) {
            MDC.clear();
        } else {
            MDC.setContextMap(context);
        }
    }

}

2.2 攔截器和過濾器

2.2.1 攔截器定義和配置

package demo;

import org.slf4j.MDC;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;

public class RequestInterceptor extends HandlerInterceptorAdapter {

    private static final List<String> paramSet = Arrays.asList("traceId");

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        this.setParam(request);
        return super.preHandle(request, response, handler);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        MDC.clear();
    }

    private void setParam(HttpServletRequest request) {
        // 設置要放到MDC中的參數(shù)
        for (String key : paramSet) {
            String val = request.getHeader(key);
            if (!StringUtils.isEmpty(val)) {
                MDC.put(key, val);
            }
        }
    }

}

攔截器配置

import demo.RequestInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 攔截WEB請求
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RequestInterceptor());
    }

}

2.2.2 過濾器配置

@Component
public class TraceIdFilter extends OncePerRequestFilter{
    private static final String TRACE_ID = "traceId";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {
        // 跳過預檢請求(OPTIONS)
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            filterChain.doFilter(request, response);
            return;
        }
        try {
            String traceId = UUID.randomUUID().toString().replace("-", "");
            MDC.put(TRACE_ID, traceId);
            filterChain.doFilter(request, response);
        } finally {
            MDC.remove(TRACE_ID);
        }
    }
}

2.3 Java線程池中使用

2.3.1 配置線程池

@Configuration
public class ThreadPoolService {
    @Bean
    public ThreadPoolExecutor threadPoolExecutor() {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
                (4, 8, 10,
                TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(5536),
                new ScheduledThreadFactory("demo-"), new ThreadPoolExecutor.CallerRunsPolicy());
        return threadPoolExecutor;
    }   
}

點擊了解線程池相關信息

2.3.2 使用ExecutorCompletionService方式

使用ExecutorCompletionService實現(xiàn)多線程調用
點擊了解更多關于ExecutorCompletionService信息

/**
 * 使用MDC傳遞traceId
 */
public class Demo {

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    public void demo() {
        ExecutorCompletionService ecs = new ExecutorCompletionService(threadPoolExecutor);
        ecs.submit(MDCUtil.wrap(new TestMDC(), MDC.getCopyOfContextMap()));
    }
    
    class TestMDC implements Callable {
        @Override
        public Object call() throws Exception {
            // TODO 代碼邏輯
            return null;
        }
    }
}

2.3.3 使用CompletableFuture方式

使用CompletableFuture實現(xiàn)多線程調用,其中收集CompletableFuture運行結果,
點擊了解更多關于CompletableFuture信息

public class Result {}
/**
 * 使用MDC傳遞traceId
 */
public class Demo {

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    private CompletableFuture<Result> test() {
    
        Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
        
        return CompletableFuture.supplyAsync(() -> {
        
            // 必須在打印日志前設置
            MDCUtil.setMDCContextMap(copyOfContextMap);
            //MDC.put("subTraceId",''); //如果需要對子線程進行加線程跟蹤號,可在此處設定
            // TODO 業(yè)務邏輯
            return new Result();
            
        }, threadPoolExecutor).exceptionally(new Function<Throwable, Result>() {
            /**捕捉異常,不會導致整個流程中斷**/
            @Override
            public Result apply(Throwable throwable) {
                log.error("線程[{}]發(fā)生了異常[{}], 繼續(xù)執(zhí)行其他線程", Thread.currentThread().getName(), throwable.getMessage());
                return null;
            }
        });
    }
}

2.4 Spring線程池中使用

2.4.1 繼承ThreadPoolTaskExecutor

public class ThreadPoolMdcWrapper extends ThreadPoolTaskExecutor {

    public ThreadPoolMdcWrapper() {

    }

    @Override
    public void execute(Runnable task) {
        super.execute(MDCUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public void execute(Runnable task, long startTimeout) {
        super.execute(MDCUtil.wrap(task, MDC.getCopyOfContextMap()), startTimeout);
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(MDCUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(MDCUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public ListenableFuture<?> submitListenable(Runnable task) {
        return super.submitListenable(MDCUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
        return super.submitListenable(MDCUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}

2.4.2 配置線程池

繼承ThreadPoolTaskExecutor ,重寫線程執(zhí)行的方法。到這我們就做完了大部分的準備工作,還剩下最關鍵的就是讓程序用到我們封裝后的線程池。我們可以在聲明線程池的時候,直接使用我們封裝好的線程池(因為繼承了ThreadPoolTaskExecutor)
點擊了解Spring線程池配置

@Bean
public ThreadPoolTaskExecutor taskExecutor() {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolMdcWrapper();
    //核心線程數(shù),默認為1
    taskExecutor.setCorePoolSize(1);
    //最大線程數(shù),默認為Integer.MAX_VALUE
    taskExecutor.setMaxPoolSize(200);
    //隊列最大長度,一般需要設置值>=notifyScheduledMainExecutor.maxNum;默認為Integer.MAX_VALUE
    taskExecutor.setQueueCapacity(2000);
    //線程池維護線程所允許的空閑時間,默認為60s
    taskExecutor.setKeepAliveSeconds(60);
    //線程池對拒絕任務(無線程可用)的處理策略
    taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
    // 初始化線程池
    taskExecutor.initialize();
    return  taskExecutor;
}

到這我們所做的準備工作,改造工作也就結束了,剩下的就是使用了。只要在程序異步調用時,利用聲明好的taskExecutor線程池進行調用,就可以在線程上下文正確傳遞Traceid了

2.5 異步線程 AsyncConfigurer

繼承了SpringAsyncConfigurer,并重寫了getAsyncExecutor方法,這樣在Spring中使用@Async注解開啟異步線程,會自動傳遞MDC信息給子線程,

另外關于異步線程的異常捕獲,先列舉一下一般開啟異步的方式:

  • 使用Spring@Async注解開啟異步
  • 通過executor.execute開啟異步
  • 通過executor.submit開啟異步
  • 通過CompletableFuture開啟異步

下面針對異步子線程的異常捕獲提供幾種解決方案:

  • 重寫AsyncConfigurer的getAsyncUncaughtExceptionHandler方法,這種方式只能捕獲方式A開啟的異步
  • 使用Future.get(),可以捕獲方式C開啟的異步
  • 使用Completable.join()或者Completable.get(),可以捕獲方式D開啟的異步
  • 重寫getAsyncExecutor方法時,在runnable.run()代碼塊上使用try/catch,可以捕獲方式A,B,C開啟的異步
  • 使用try/catch包裹整個runnable函數(shù)式接口,這樣可以捕獲A,B,C,D開啟的異步
    executor.execute(() -> {
            try {
                //需要開啟異步的業(yè)務邏輯方法或者代碼塊
                xxx();
            } catch (Throwable e) {
                log.error("異常", e);
            }
        });

下面給出完整的代碼

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
 
@Slf4j
@EnableAsync
@Configuration
@RequiredArgsConstructor
public class ThreadPoolTaskConfig implements AsyncConfigurer {
 
    @Bean("AsyncExecutor")
    @Override
    public ThreadPoolTaskExecutor getAsyncExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor() {
            /**
             * 所有線程都會委托給這個execute方法,在這個方法中我們把父線程的MDC內(nèi)容賦值給子線程
             * https://logback.qos.ch/manual/mdc.html#managedThreads
             *
             * @param runnable runnable
             */
            @Override
            public void execute(Runnable runnable) {
                // 獲取父線程MDC中的內(nèi)容,必須在run方法之前,否則等異步線程執(zhí)行的時候有可能MDC里面的值已經(jīng)被清空了,這個時候就會返回null
                Map<String, String> context = MDC.getCopyOfContextMap();
                super.execute(() -> {
                    // 將父線程的MDC內(nèi)容傳給子線程
                    if (context != null) {
                        MDC.setContextMap(context);
                    }
                    try {
                        // 執(zhí)行異步操作
                        runnable.run();
                    } catch (Throwable e) {
                        log.info("異步線程執(zhí)行異常:{}", e.getMessage(), e);
                        //替換成業(yè)務異常
                        throw new RuntimeException("異步線程執(zhí)行異常");
                    } finally {
                        // 清空MDC內(nèi)容
                        MDC.clear();
                    }
                });
            }
 
            @Override
            public <T> Future<T> submit(Callable<T> task) {
                // 獲取父線程MDC中的內(nèi)容,必須在run方法之前,否則等異步線程執(zhí)行的時候有可能MDC里面的值已經(jīng)被清空了,這個時候就會返回null
                Map<String, String> context = MDC.getCopyOfContextMap();
                return super.submit(() -> {
                    // 將父線程的MDC內(nèi)容傳給子線程
                    if (context != null) {
                        MDC.setContextMap(context);
                    }
                    try {
                        // 執(zhí)行異步操作
                        return task.call();
                    } catch (Throwable e) {
                        log.info("異步線程執(zhí)行異常:{}", e.getMessage(), e);
                        //替換成業(yè)務異常
                        throw new RuntimeException("異步線程執(zhí)行異常");
                    } finally {
                        // 清空MDC內(nèi)容
                        MDC.clear();
                    }
                });
            }
        };
        ;
        // 設置核心線程數(shù)
        threadPoolTaskExecutor.setCorePoolSize(30);
        // 設置最大線程數(shù)
        threadPoolTaskExecutor.setMaxPoolSize(50);
        // 設置隊列容量
        threadPoolTaskExecutor.setQueueCapacity(1000);
        // 設置線程活躍時間(秒)
        threadPoolTaskExecutor.setKeepAliveSeconds(60);
        // 設置拒絕策略
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 設置線程池終止等待時間
        threadPoolTaskExecutor.setAwaitTerminationSeconds(10);
 
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }
 
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (Throwable throwable, Method method, Object... objects) -> {
            log.error("AsyncUncaughtExceptionHandler: ", throwable);
            log.info("method: {}", method.getName());
            log.info("objects: {}", objects);
        };
    }
}

調用測試

@Slf4j
@RestController
public class UserController {
    @Autowired
    @Qualifier("asyncExe")
    private Executor executor;
    @Autowired
    private AsyncServiceImpl ayncService;
    @GetMapping("/t1")
    public void test1(){
        log.info("開始....");
        CompletableFuture.runAsync(() ->{
            log.info("異步中....");
        });
        executor.execute(() ->{
            log.info("線程池中....");
        });
        ayncService.test();
        log.info("結束....");
    }
}   

@Slf4j
@Service
public class AsyncServiceImpl {
    @Async("asyncExe")
    public void test(){
        //...具體業(yè)務邏輯
        log.info("異步async中....");
    }
}

2.6 多線程間傳遞 TransmittableThreadLocal

2.6.1 引言

假如使用logback/log4j官網(wǎng)推薦的方案,顯示調用 MDC.getCopyOfContextMap()MDC.setContextMap() ,在向線程池提交任務的時候需要顯示的去調用。這種方式很繁瑣,而且侵入性很高,可維護性也很低。

如果使用阿里的TransmittableThreadLocal方案,是使用TransmittableThreadLocal的實現(xiàn)去增強ThreadPoolExecutor,不需要在任務提交運行的時候去顯示的調用MDC,但是TransmittableThreadLocal的官網(wǎng)上沒有明確的結合MDC的教程。
主要有2種,一種是自己實現(xiàn)一個MDCAdapter替換logback/log4j的MDCAdapter,內(nèi)部將其ThreadLocal替換為TransmittableThreadLocal的實現(xiàn),在通過其他方式注入到日志框架中。
另外一種方式是使用 logback-mdc-ttl 來更換項目中的logback框架,內(nèi)部的思路和上面類似,也是替換了MDCAdapter的實現(xiàn)。
但是這2種方式都有很大的問題,第一種需要修改日志框架的注入實現(xiàn),在后續(xù)升級日志框架有很大的風險。第二種方式是引入了一個三方的日志框架,不可維護。

2.6.2 解決方案

總結來看上述幾種解決方案都不太理解,第二種方式雖然使用了TransmittableThreadLocal解決了包裝類的問題,但是沒有很好的適配MDC,修改了大量的實現(xiàn)代碼,而且不利于后續(xù)的升級維護。
在搜索的相關的資料、源碼以及TransmittableThreadLocal的issue里,發(fā)現(xiàn)了一種比較簡潔的實現(xiàn)方式。

添加 HandlerInterceptor 攔截器,核心的實現(xiàn)思路是實現(xiàn) TransmittableThreadLocalinitialValue,beforeExecute,afterExecute接口,在多線程數(shù)據(jù)傳遞的時候,將數(shù)據(jù)復制一份給MDC。

@Component
public class TraceIdInterceptor implements HandlerInterceptor {

    /**
     * 實現(xiàn) TransmittableThreadLocal 的 initialValue,beforeExecute,afterExecute接口
     */
    static TransmittableThreadLocal<Map<String, String>> ttlMDC = new TransmittableThreadLocal<>() {
        /**
         * 在多線程數(shù)據(jù)傳遞的時候,將數(shù)據(jù)復制一份給MDC
         */
        @Override
        protected void beforeExecute() {
            final Map<String, String> mdc = get();
            mdc.forEach(MDC::put);
        }

        @Override
        protected void afterExecute() {
            MDC.clear();
        }

        @Override
        protected Map<String, String> initialValue() {
            return Maps.newHashMap();
        }
    };


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        //MDC記錄traceId
        String traceId = IdUtil.fastUUID();
        MDC.put("traceId", traceId);

        //同時給TransmittableThreadLocal記錄traceId
        ttlMDC.get().put("traceId", traceId);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
        @Nullable Exception ex) {

        //清除數(shù)據(jù)
        MDC.clear();
        ttlMDC.get().clear();
        ttlMDC.remove();
    }
}

使用 TransmittableThreadLocal 提供的包裝池,

@Bean
public Executor asyncExecutor() {
    log.info("start asyncExecutor");
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    //配置核心線程數(shù)
    executor.setCorePoolSize(10);
    //配置最大線程數(shù)
    executor.setMaxPoolSize(50);
    //配置隊列大小
    executor.setQueueCapacity(0);
    //配置線程池中的線程的名稱前綴
    executor.setThreadNamePrefix("async-service-");

    // rejection-policy:當pool已經(jīng)達到max size的時候,如何處理新任務
    // CALLER_RUNS:不在新線程中執(zhí)行任務,而是有調用者所在的線程來執(zhí)行
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    //執(zhí)行初始化
    executor.initialize();
    //使用TransmittableThreadLocal提供的包裝池
    return TtlExecutors.getTtlExecutor(executor);
}

2.7 HTTP調用丟失traceId

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

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

2.7.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);
        //當前線程調用中有traceId,則將該traceId進行透傳
        if (traceId != null) {
            //添加請求體
            httpRequest.addHeader(Constants.TRACE_ID, traceId);
        }
    }
}

實現(xiàn) HttpRequestInterceptor接口并重寫process方法
如果調用線程中含有traceId,則需要將獲取到的traceId通過request中的header向下透傳下去

為HttpClient添加攔截器

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

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

2.7.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差不多,如果能夠獲取到當前線程的traceId則向下透傳

為OkHttp添加攔截器,調用addNetworkInterceptor方法添加攔截器

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

2.7.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方法,其余邏輯都是一樣的不重復說明

為RestTemplate添加攔截器,調用setInterceptors方法添加攔截器

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

2.8 定時任務

僅處理XXL-Job的定時任務,利用全局 AOP 切面自動加 traceId,避免每個定時任務都去加

@Aspect
@Component
public class XxlJobTraceAspect{
    private static final String TRACE_ID = "traceId";
    @Pointcut("@annotation(com.xxl.job.core.handler.annotation.XxlJob)")
    public void xxlJobMethods(){}

    @Around("xxlJobMethods()")
    public Object aroundXxlJob(ProceedingJoinPoint joinPoint)throws Throwable {
        String traceId = UUID.randomUUID().toString().replace("-", "");
        try {
            MDC.put(TRACE_ID, traceId);
            return joinPoint.proceed();
        }catch (Exception e){
            log.error("xxl-job執(zhí)行異常:{}",e.getMessage(),e);
        }finally {
            MDC.remove(TRACE_ID);
        }
        return null;
    }
}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容