日志追蹤對于接口故障排查非常重要,可以有效、快捷的定位故障點,但在多線程環(huán)境中,若沒有相關框架的支持,想要實現(xiàn)日志追蹤,就需要編碼實現(xiàn)將主線程的日志參數(shù)傳遞給子線程,本文就在線程池場景下借助MDC實現(xiàn)了traceId參數(shù)的透傳
1 MDC
1.1 簡介
MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 和 logback 提供的一種方便在多線程條件下記錄日志的功能。某些應用程序采用多線程的方式來處理多個用戶的請求。在一個用戶的使用過程中,可能有多個不同的線程來進行處理。典型的例子是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里面key為traceId的value,如果所定義的字段在MDC不存在對應的key,那么將不會打印,會留一個占位符
點擊了解Loback.xml文件解釋
1.3 MDC實現(xiàn)原理
MDC 是 SLF4J/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工具類,支持Runnable和Callable兩種,目的就是為了把父線程的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
繼承了Spring的AsyncConfigurer,并重寫了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) TransmittableThreadLocal 的 initialValue,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;
}
}