在前面寫的一篇文章中,熱心網友【地藏Kelvin】評論說在多線程中仍是有可能會亂掉,建議經過MDC打印traceId來個全鏈路調用跟蹤。掘金裏個個都是人才,說話又好聽,超喜歡在裏面。掘金使我進步,熱心網友總能提出改進意見java
經過本文將瞭解到什麼是MDC、MDC應用中存在的問題、如何解決存在的問題git
MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 、logback及log4j2 提供的一種方便在多線程條件下記錄日誌的功能。MDC 能夠當作是一個與當前線程綁定的哈希表,能夠往其中添加鍵值對。MDC 中包含的內容能夠被同一線程中執行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內容。當須要記錄日誌時,只須要從 MDC 中獲取所需的信息便可。MDC 的內容則由程序在適當的時候保存進去。對於一個 Web 應用來講,一般是在請求被處理的最開始保存這些數據github
暫時只能想到這一點spring
添加攔截器後端
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);
}
}
複製代碼
修改日誌格式springboot
<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將獲取不到 多線程
子線程中打印日誌丟失traceIdapp
HTTP調用丟失traceIdide
......丟失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()));
}
}
複製代碼
說明:
線程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爲例】:
代碼等同於如下寫法,會更直觀
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就是同樣的了
在使用HTTP調用第三方服務接口時traceId將丟失,須要對HTTP調用工具進行改造,在發送時在request header中添加traceId,在下層被調用方添加攔截器獲取header中的traceId添加到MDC中
HTTP調用有多種方式,比較常見的有HttpClient、OKHttp、RestTemplate,因此只給出這幾種HTTP調用的解決方式
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);
}
}
複製代碼
說明:
除了須要添加攔截器以外,還須要在日誌格式中添加traceId的打印,以下:
<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>
複製代碼
須要添加%X{traceId}
最後附:項目代碼,歡迎fork與star,漲點小星星,卑微乞討
1.寫個日誌請求切面,先後端甩鍋更方便
2.爲何阿里巴巴要禁用Executors建立線程池?
參考文章:
1.在Java項目中使用traceId跟蹤請求全流程日誌
2.MDC介紹 -- 一種多線程下日誌管理實踐方式