SpringBoot 項目添加 MDC 日志鏈路追蹤的執行流程
日志鏈路追蹤的意思就是將一個標志跨線程進行傳遞,在一般的小項目中也就是在你新起一個線程的時候,或者使用線程池執行任務的時候會用到,比如追蹤一個用戶請求的完整執行流程。
這裡用到MDC
和ThreadLocal
,分別由下面的包提供:
java.lang.ThreadLocal org.slf4j.MDC
直接上代碼:
1. 線程池配置
如果你直接通過手動新建線程來執行異步任務,想要實現標志傳遞的話,需要自己去實現,其實和線程池一樣,也是調用MDC
的相關方法,如下所示:
//取出父線程的MDC Map<String, String> context = MDC.getCopyOfContextMap(); //將父線程的MDC內容傳給子線程 MDC.setContextMap(context);
首先提供一個常量:
package com.example.demo.common.constant; /** * 常量 * * @author wangbo * @date 2021/5/13 */ public class Constants { public static final String LOG_MDC_ID = "trace_id"; }
接下來需要對ThreadPoolTaskExecutor
的方法進行重寫:
package com.example.demo.common.threadpool; import com.example.demo.common.constant.Constants; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.Map; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.Future; /** * MDC線程池 * 實現內容傳遞 * * @author wangbo * @date 2021/5/13 */ @Slf4j public class MdcTaskExecutor extends ThreadPoolTaskExecutor { @Override public <T> Future<T> submit(Callable<T> task) { log.info("mdc thread pool task executor submit"); Map<String, String> context = MDC.getCopyOfContextMap(); return super.submit(() -> { T result; if (context != null) { //將父線程的MDC內容傳給子線程 MDC.setContextMap(context); } else { //直接給子線程設置MDC MDC.put(Constants.LOG_MDC_ID, UUID.randomUUID().toString().replace("-", "")); } try { //執行任務 result = task.call(); } finally { try { MDC.clear(); } catch (Exception e) { log.warn("MDC clear exception", e); } } return result; }); } @Override public void execute(Runnable task) { log.info("mdc thread pool task executor execute"); Map<String, String> context = MDC.getCopyOfContextMap(); super.execute(() -> { if (context != null) { //將父線程的MDC內容傳給子線程 MDC.setContextMap(context); } else { //直接給子線程設置MDC MDC.put(Constants.LOG_MDC_ID, UUID.randomUUID().toString().replace("-", "")); } try { //執行任務 task.run(); } finally { try { MDC.clear(); } catch (Exception e) { log.warn("MDC clear exception", e); } } }); } }
然後使用自定義的重寫子類MdcTaskExecutor
來實現線程池配置:
package com.example.demo.common.threadpool; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; /** * 線程池配置 * * @author wangbo * @date 2021/5/13 */ @Slf4j @Configuration public class ThreadPoolConfig { /** * 異步任務線程池 * 用於執行普通的異步請求,帶有請求鏈路的MDC標志 */ @Bean public Executor commonThreadPool() { log.info("start init common thread pool"); //ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); MdcTaskExecutor executor = new MdcTaskExecutor(); //配置核心線程數 executor.setCorePoolSize(10); //配置最大線程數 executor.setMaxPoolSize(20); //配置隊列大小 executor.setQueueCapacity(3000); //配置空閑線程存活時間 executor.setKeepAliveSeconds(120); //配置線程池中的線程的名稱前綴 executor.setThreadNamePrefix("common-thread-pool-"); //當達到最大線程池的時候丟棄最老的任務 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); //執行初始化 executor.initialize(); return executor; } /** * 定時任務線程池 * 用於執行自啟動的任務執行,父線程不帶有MDC標志,不需要傳遞,直接設置新的MDC * 和上面的線程池沒啥區別,隻是名字不同 */ @Bean public Executor scheduleThreadPool() { log.info("start init schedule thread pool"); MdcTaskExecutor executor = new MdcTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(20); executor.setQueueCapacity(3000); executor.setKeepAliveSeconds(120); executor.setThreadNamePrefix("schedule-thread-pool-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); executor.initialize(); return executor; } }
2. 攔截器配置
package com.example.demo.common.interceptor; import com.example.demo.common.constant.Constants; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.UUID; /** * 日志攔截器 * * @author wangbo * @date 2021/5/13 */ @Slf4j @Component public class LogInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //log.info("進入 LogInterceptor"); //添加MDC值 MDC.put(Constants.LOG_MDC_ID, UUID.randomUUID().toString().replace("-", "")); //打印接口請求信息 String method = request.getMethod(); String uri = request.getRequestURI(); log.info("[請求接口] : {} : {}", method, uri); //打印請求參數 return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { //log.info("執行 LogInterceptor"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //log.info("退出 LogInterceptor"); //打印請求結果 //刪除MDC值 MDC.remove(Constants.LOG_MDC_ID); } }
對攔截器進行註冊:
package com.example.demo.common.config; import com.example.demo.common.interceptor.LogInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * MVC配置 * * @author wangbo * @date 2021/5/13 */ @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private LogInterceptor logInterceptor; /** * 攔截器註冊 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(logInterceptor); } }
3. 日志文件配置
需要在logback-spring.xml
文件中的日志打印格式裡添加%X{trace_id}
,如下所示:
<!-- 控制臺打印日志的相關配置 --> <appender name="console_out" class="ch.qos.logback.core.ConsoleAppender"> <!-- 日志格式 --> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{trace_id}] [%level] [%thread] [%class:%line] - %m%n</pattern> <charset>UTF-8</charset> </encoder> </appender>
4. 使用方法示例
4.1. 異步使用
這裡註意,異步方法的調用不能直接調用當前類的方法,也就是說調用方法和異步方法不能在同一個類裡,否則會變為同步執行。
/** * 異步方法 */ //@Async//這種寫法,當隻有一個線程池時,會使用該線程池執行,有多個則會使用SimpleAsyncTaskExecutor @Async(value = "commonThreadPool")//指定執行的線程池 @Override public void async() { log.info("測試異步線程池"); }
4.2. 定時任務
package com.example.demo.generator.crontab; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.time.LocalDateTime; /** * 定時任務 * * @author wangbo * @date 2021/5/14 */ @Slf4j @Component public class TestTimeTask { //基於註解@Scheduled默認為單線程,開啟多個任務時,任務的執行時機會受上一個任務執行時間的影響。 //使用的線程池是taskScheduler,線程ID為scheduling-x //添加@Async註解指定線程池,則可以多線程執行定時任務(原本是單線程的)。 /** * 兩次任務開始的時間間隔為2S * 不使用線程池,單線程間隔則為4S。單線程保證不瞭這個2S間隔,因為任務執行耗時超過瞭定時間隔,就會影響下一次任務的執行 * 使用線程池,多線程執行,時間間隔為2S */ //@Async(value = "scheduleThreadPool") //@Scheduled(fixedRate = 2000) public void fixedRate() { log.info("定時間隔任務 fixedRate = {}", LocalDateTime.now()); try { Thread.sleep(4_000); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 下次任務的開始時間距離上次任務的結束時間間隔為2S * 這種適合使用單線程,不適合使用線程池,單線程間隔則為6S。 * 用瞭線程池,和這個特性相背離瞭 */ //@Scheduled(fixedDelay = 2_000) public void fixedDelay() { log.info("延遲定時間隔任務 fixedDelay = {}", LocalDateTime.now()); try { Thread.sleep(4_000); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 首次延遲10S後執行fixedDelay類型間隔任務,也可以配置為fixedDelay類型間隔任務 * 控件第一次執行之前要延遲的毫秒數 * {@link # fixeddrate} or {@link #fixedDelay} */ //@Scheduled(initialDelay = 10_000, fixedDelay = 1_000) public void initialDelay() { log.info("首次延遲定時間隔任務 initialDelay = {}", LocalDateTime.now()); } /** * 這裡使用線程池也是為瞭防止任務執行耗時超過瞭定時間隔,就會影響下一次任務的執行 */ //@Async(value = "scheduleThreadPool") //@Scheduled(cron = "0/2 * * * * *") public void testCron() { log.info("測試表達式定時任務 testCron = {}", LocalDateTime.now()); try { Thread.sleep(4_000); } catch (InterruptedException e) { e.printStackTrace(); } } }
到此這篇關於SpringBoot 項目添加 MDC 日志鏈路追蹤的文章就介紹到這瞭,更多相關SpringBoot MDC 日志鏈路追蹤內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- springboot動態調用實現類方式
- Java創建多線程的8種方式集合
- springboot @Async 註解如何實現方法異步
- 線程池滿Thread pool exhausted排查和解決方案
- Java並發編程之Executor接口的使用