SpringBoot 項目添加 MDC 日志鏈路追蹤的執(zhí)行流程
日志鏈路追蹤的意思就是將一個標(biāo)志跨線程進行傳遞,在一般的小項目中也就是在你新起一個線程的時候,或者使用線程池執(zhí)行任務(wù)的時候會用到,比如追蹤一個用戶請求的完整執(zhí)行流程。
這里用到MDC
和ThreadLocal
,分別由下面的包提供:
java.lang.ThreadLocal org.slf4j.MDC
直接上代碼:
1. 線程池配置
如果你直接通過手動新建線程來執(zhí)行異步任務(wù),想要實現(xiàn)標(biāo)志傳遞的話,需要自己去實現(xiàn),其實和線程池一樣,也是調(diào)用MDC
的相關(guān)方法,如下所示:
//取出父線程的MDC Map<String, String> context = MDC.getCopyOfContextMap(); //將父線程的MDC內(nèi)容傳給子線程 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線程池 * 實現(xiàn)內(nèi)容傳遞 * * @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內(nèi)容傳給子線程 MDC.setContextMap(context); } else { //直接給子線程設(shè)置MDC MDC.put(Constants.LOG_MDC_ID, UUID.randomUUID().toString().replace("-", "")); } try { //執(zhí)行任務(wù) 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內(nèi)容傳給子線程 MDC.setContextMap(context); } else { //直接給子線程設(shè)置MDC MDC.put(Constants.LOG_MDC_ID, UUID.randomUUID().toString().replace("-", "")); } try { //執(zhí)行任務(wù) task.run(); } finally { try { MDC.clear(); } catch (Exception e) { log.warn("MDC clear exception", e); } } }); } }
然后使用自定義的重寫子類MdcTaskExecutor
來實現(xiàn)線程池配置:
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 { /** * 異步任務(wù)線程池 * 用于執(zhí)行普通的異步請求,帶有請求鏈路的MDC標(biāo)志 */ @Bean public Executor commonThreadPool() { log.info("start init common thread pool"); //ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); MdcTaskExecutor executor = new MdcTaskExecutor(); //配置核心線程數(shù) executor.setCorePoolSize(10); //配置最大線程數(shù) executor.setMaxPoolSize(20); //配置隊列大小 executor.setQueueCapacity(3000); //配置空閑線程存活時間 executor.setKeepAliveSeconds(120); //配置線程池中的線程的名稱前綴 executor.setThreadNamePrefix("common-thread-pool-"); //當(dāng)達到最大線程池的時候丟棄最老的任務(wù) executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); //執(zhí)行初始化 executor.initialize(); return executor; } /** * 定時任務(wù)線程池 * 用于執(zhí)行自啟動的任務(wù)執(zhí)行,父線程不帶有MDC標(biāo)志,不需要傳遞,直接設(shè)置新的MDC * 和上面的線程池沒啥區(qū)別,只是名字不同 */ @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); //打印請求參數(shù) return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { //log.info("執(zhí)行 LogInterceptor"); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { //log.info("退出 LogInterceptor"); //打印請求結(jié)果 //刪除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}
,如下所示:
<!-- 控制臺打印日志的相關(guān)配置 --> <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. 異步使用
這里注意,異步方法的調(diào)用不能直接調(diào)用當(dāng)前類的方法,也就是說調(diào)用方法和異步方法不能在同一個類里,否則會變?yōu)橥綀?zhí)行。
/** * 異步方法 */ //@Async//這種寫法,當(dāng)只有一個線程池時,會使用該線程池執(zhí)行,有多個則會使用SimpleAsyncTaskExecutor @Async(value = "commonThreadPool")//指定執(zhí)行的線程池 @Override public void async() { log.info("測試異步線程池"); }
4.2. 定時任務(wù)
package com.example.demo.generator.crontab; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.time.LocalDateTime; /** * 定時任務(wù) * * @author wangbo * @date 2021/5/14 */ @Slf4j @Component public class TestTimeTask { //基于注解@Scheduled默認為單線程,開啟多個任務(wù)時,任務(wù)的執(zhí)行時機會受上一個任務(wù)執(zhí)行時間的影響。 //使用的線程池是taskScheduler,線程ID為scheduling-x //添加@Async注解指定線程池,則可以多線程執(zhí)行定時任務(wù)(原本是單線程的)。 /** * 兩次任務(wù)開始的時間間隔為2S * 不使用線程池,單線程間隔則為4S。單線程保證不了這個2S間隔,因為任務(wù)執(zhí)行耗時超過了定時間隔,就會影響下一次任務(wù)的執(zhí)行 * 使用線程池,多線程執(zhí)行,時間間隔為2S */ //@Async(value = "scheduleThreadPool") //@Scheduled(fixedRate = 2000) public void fixedRate() { log.info("定時間隔任務(wù) fixedRate = {}", LocalDateTime.now()); try { Thread.sleep(4_000); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 下次任務(wù)的開始時間距離上次任務(wù)的結(jié)束時間間隔為2S * 這種適合使用單線程,不適合使用線程池,單線程間隔則為6S。 * 用了線程池,和這個特性相背離了 */ //@Scheduled(fixedDelay = 2_000) public void fixedDelay() { log.info("延遲定時間隔任務(wù) fixedDelay = {}", LocalDateTime.now()); try { Thread.sleep(4_000); } catch (InterruptedException e) { e.printStackTrace(); } } /** * 首次延遲10S后執(zhí)行fixedDelay類型間隔任務(wù),也可以配置為fixedDelay類型間隔任務(wù) * 控件第一次執(zhí)行之前要延遲的毫秒數(shù) * {@link # fixeddrate} or {@link #fixedDelay} */ //@Scheduled(initialDelay = 10_000, fixedDelay = 1_000) public void initialDelay() { log.info("首次延遲定時間隔任務(wù) initialDelay = {}", LocalDateTime.now()); } /** * 這里使用線程池也是為了防止任務(wù)執(zhí)行耗時超過了定時間隔,就會影響下一次任務(wù)的執(zhí)行 */ //@Async(value = "scheduleThreadPool") //@Scheduled(cron = "0/2 * * * * *") public void testCron() { log.info("測試表達式定時任務(wù) testCron = {}", LocalDateTime.now()); try { Thread.sleep(4_000); } catch (InterruptedException e) { e.printStackTrace(); } } }
到此這篇關(guān)于SpringBoot 項目添加 MDC 日志鏈路追蹤的文章就介紹到這了,更多相關(guān)SpringBoot MDC 日志鏈路追蹤內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java輸入時如何通過回車(enter)來結(jié)束輸入
這篇文章主要介紹了java輸入時如何通過回車(enter)來結(jié)束輸入,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-05-05Java 實現(xiàn)倒計時功能(由秒計算天、小時、分鐘、秒)
最近做項目遇到這樣的需求,天、小時、分鐘、秒的數(shù)值都是隔開的,服務(wù)器端只返回一個時間戳長度,怎么實現(xiàn)這樣的功能呢?下面小編給大家?guī)砹薐ava 實現(xiàn)倒計時功能的方案,需要的朋友參考下吧2018-01-01Java數(shù)據(jù)類型實現(xiàn)自動與強制轉(zhuǎn)換的示例代碼
Java數(shù)據(jù)類型之間的轉(zhuǎn)換有自動轉(zhuǎn)換和強制類型轉(zhuǎn)換,這篇文章主要給大家介紹Java數(shù)據(jù)類型如何實現(xiàn)自動轉(zhuǎn)換與強制轉(zhuǎn)換,需要的朋友可以參考下2023-05-05SpringSecurity rememberme功能實現(xiàn)過程解析
這篇文章主要介紹了SpringSecurity rememberme功能實現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-03-03