SpringBoot 項(xiàng)目添加 MDC 日志鏈路追蹤的執(zhí)行流程
日志鏈路追蹤的意思就是將一個(gè)標(biāo)志跨線(xiàn)程進(jìn)行傳遞,在一般的小項(xiàng)目中也就是在你新起一個(gè)線(xiàn)程的時(shí)候,或者使用線(xiàn)程池執(zhí)行任務(wù)的時(shí)候會(huì)用到,比如追蹤一個(gè)用戶(hù)請(qǐng)求的完整執(zhí)行流程。
這里用到MDC和ThreadLocal,分別由下面的包提供:
java.lang.ThreadLocal org.slf4j.MDC
直接上代碼:
1. 線(xiàn)程池配置
如果你直接通過(guò)手動(dòng)新建線(xiàn)程來(lái)執(zhí)行異步任務(wù),想要實(shí)現(xiàn)標(biāo)志傳遞的話(huà),需要自己去實(shí)現(xiàn),其實(shí)和線(xiàn)程池一樣,也是調(diào)用MDC的相關(guān)方法,如下所示:
//取出父線(xiàn)程的MDC Map<String, String> context = MDC.getCopyOfContextMap(); //將父線(xiàn)程的MDC內(nèi)容傳給子線(xiàn)程 MDC.setContextMap(context);
首先提供一個(gè)常量:
package com.example.demo.common.constant;
/**
* 常量
*
* @author wangbo
* @date 2021/5/13
*/
public class Constants {
public static final String LOG_MDC_ID = "trace_id";
}
接下來(lái)需要對(duì)ThreadPoolTaskExecutor的方法進(jìn)行重寫(xiě):
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)程池
* 實(shí)現(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) {
//將父線(xiàn)程的MDC內(nèi)容傳給子線(xiàn)程
MDC.setContextMap(context);
} else {
//直接給子線(xiàn)程設(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) {
//將父線(xiàn)程的MDC內(nèi)容傳給子線(xiàn)程
MDC.setContextMap(context);
} else {
//直接給子線(xiàn)程設(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);
}
}
});
}
}
然后使用自定義的重寫(xiě)子類(lèi)MdcTaskExecutor來(lái)實(shí)現(xiàn)線(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;
/**
* 線(xiàn)程池配置
*
* @author wangbo
* @date 2021/5/13
*/
@Slf4j
@Configuration
public class ThreadPoolConfig {
/**
* 異步任務(wù)線(xiàn)程池
* 用于執(zhí)行普通的異步請(qǐng)求,帶有請(qǐng)求鏈路的MDC標(biāo)志
*/
@Bean
public Executor commonThreadPool() {
log.info("start init common thread pool");
//ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
MdcTaskExecutor executor = new MdcTaskExecutor();
//配置核心線(xiàn)程數(shù)
executor.setCorePoolSize(10);
//配置最大線(xiàn)程數(shù)
executor.setMaxPoolSize(20);
//配置隊(duì)列大小
executor.setQueueCapacity(3000);
//配置空閑線(xiàn)程存活時(shí)間
executor.setKeepAliveSeconds(120);
//配置線(xiàn)程池中的線(xiàn)程的名稱(chēng)前綴
executor.setThreadNamePrefix("common-thread-pool-");
//當(dāng)達(dá)到最大線(xiàn)程池的時(shí)候丟棄最老的任務(wù)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
//執(zhí)行初始化
executor.initialize();
return executor;
}
/**
* 定時(shí)任務(wù)線(xiàn)程池
* 用于執(zhí)行自啟動(dòng)的任務(wù)執(zhí)行,父線(xiàn)程不帶有MDC標(biāo)志,不需要傳遞,直接設(shè)置新的MDC
* 和上面的線(xiàn)程池沒(méi)啥區(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("進(jìn)入 LogInterceptor");
//添加MDC值
MDC.put(Constants.LOG_MDC_ID, UUID.randomUUID().toString().replace("-", ""));
//打印接口請(qǐng)求信息
String method = request.getMethod();
String uri = request.getRequestURI();
log.info("[請(qǐng)求接口] : {} : {}", method, uri);
//打印請(qǐng)求參數(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");
//打印請(qǐng)求結(jié)果
//刪除MDC值
MDC.remove(Constants.LOG_MDC_ID);
}
}
對(duì)攔截器進(jìn)行注冊(cè):
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;
/**
* 攔截器注冊(cè)
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor);
}
}
3. 日志文件配置
需要在logback-spring.xml文件中的日志打印格式里添加%X{trace_id},如下所示:
<!-- 控制臺(tái)打印日志的相關(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)前類(lèi)的方法,也就是說(shuō)調(diào)用方法和異步方法不能在同一個(gè)類(lèi)里,否則會(huì)變?yōu)橥綀?zhí)行。
/**
* 異步方法
*/
//@Async//這種寫(xiě)法,當(dāng)只有一個(gè)線(xiàn)程池時(shí),會(huì)使用該線(xiàn)程池執(zhí)行,有多個(gè)則會(huì)使用SimpleAsyncTaskExecutor
@Async(value = "commonThreadPool")//指定執(zhí)行的線(xiàn)程池
@Override
public void async() {
log.info("測(cè)試異步線(xiàn)程池");
}
4.2. 定時(shí)任務(wù)
package com.example.demo.generator.crontab;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 定時(shí)任務(wù)
*
* @author wangbo
* @date 2021/5/14
*/
@Slf4j
@Component
public class TestTimeTask {
//基于注解@Scheduled默認(rèn)為單線(xiàn)程,開(kāi)啟多個(gè)任務(wù)時(shí),任務(wù)的執(zhí)行時(shí)機(jī)會(huì)受上一個(gè)任務(wù)執(zhí)行時(shí)間的影響。
//使用的線(xiàn)程池是taskScheduler,線(xiàn)程ID為scheduling-x
//添加@Async注解指定線(xiàn)程池,則可以多線(xiàn)程執(zhí)行定時(shí)任務(wù)(原本是單線(xiàn)程的)。
/**
* 兩次任務(wù)開(kāi)始的時(shí)間間隔為2S
* 不使用線(xiàn)程池,單線(xiàn)程間隔則為4S。單線(xiàn)程保證不了這個(gè)2S間隔,因?yàn)槿蝿?wù)執(zhí)行耗時(shí)超過(guò)了定時(shí)間隔,就會(huì)影響下一次任務(wù)的執(zhí)行
* 使用線(xiàn)程池,多線(xiàn)程執(zhí)行,時(shí)間間隔為2S
*/
//@Async(value = "scheduleThreadPool")
//@Scheduled(fixedRate = 2000)
public void fixedRate() {
log.info("定時(shí)間隔任務(wù) fixedRate = {}", LocalDateTime.now());
try {
Thread.sleep(4_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 下次任務(wù)的開(kāi)始時(shí)間距離上次任務(wù)的結(jié)束時(shí)間間隔為2S
* 這種適合使用單線(xiàn)程,不適合使用線(xiàn)程池,單線(xiàn)程間隔則為6S。
* 用了線(xiàn)程池,和這個(gè)特性相背離了
*/
//@Scheduled(fixedDelay = 2_000)
public void fixedDelay() {
log.info("延遲定時(shí)間隔任務(wù) fixedDelay = {}", LocalDateTime.now());
try {
Thread.sleep(4_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 首次延遲10S后執(zhí)行fixedDelay類(lèi)型間隔任務(wù),也可以配置為fixedDelay類(lèi)型間隔任務(wù)
* 控件第一次執(zhí)行之前要延遲的毫秒數(shù)
* {@link # fixeddrate} or {@link #fixedDelay}
*/
//@Scheduled(initialDelay = 10_000, fixedDelay = 1_000)
public void initialDelay() {
log.info("首次延遲定時(shí)間隔任務(wù) initialDelay = {}", LocalDateTime.now());
}
/**
* 這里使用線(xiàn)程池也是為了防止任務(wù)執(zhí)行耗時(shí)超過(guò)了定時(shí)間隔,就會(huì)影響下一次任務(wù)的執(zhí)行
*/
//@Async(value = "scheduleThreadPool")
//@Scheduled(cron = "0/2 * * * * *")
public void testCron() {
log.info("測(cè)試表達(dá)式定時(shí)任務(wù) testCron = {}", LocalDateTime.now());
try {
Thread.sleep(4_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
到此這篇關(guān)于SpringBoot 項(xiàng)目添加 MDC 日志鏈路追蹤的文章就介紹到這了,更多相關(guān)SpringBoot MDC 日志鏈路追蹤內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Springboot MDC+logback實(shí)現(xiàn)日志追蹤的方法
- SpringBoot利用MDC機(jī)制過(guò)濾單次請(qǐng)求的所有日志
- SpringBoot項(xiàng)目使用slf4j的MDC日志打點(diǎn)功能(最新推薦)
- SpringBoot MDC全鏈路調(diào)用日志跟蹤實(shí)現(xiàn)詳解
- SpringBoot+MDC實(shí)現(xiàn)鏈路調(diào)用日志的方法
- Springboot+MDC+traceId日志中打印唯一traceId
- SpringBoot項(xiàng)目使用MDC給日志增加唯一標(biāo)識(shí)的實(shí)現(xiàn)步驟
相關(guān)文章
使用HttpClient實(shí)現(xiàn)文件的上傳下載方法
下面小編就為大家?guī)?lái)一篇使用HttpClient實(shí)現(xiàn)文件的上傳下載方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-12-12
Java面向?qū)ο笾甪inal關(guān)鍵字詳細(xì)解讀
這篇文章主要介紹了Java面向?qū)ο笾甪inal關(guān)鍵字詳細(xì)解讀,final修飾的屬性又叫常量,一般用 XX_XX_XX來(lái)命名,final修飾的屬性在定義時(shí)必須賦初始值,并且以后不能再修改,需要的朋友可以參考下2024-01-01
SpringBoot使用Scheduling實(shí)現(xiàn)定時(shí)任務(wù)的示例代碼
Spring Boot提供了一種方便的方式來(lái)實(shí)現(xiàn)定時(shí)任務(wù),即使用Spring的@Scheduled注解,通過(guò)在方法上添加@Scheduled注解,我們可以指定方法在何時(shí)執(zhí)行,本文我們就給大家介紹一下SpringBoot如何使用Scheduling實(shí)現(xiàn)定時(shí)任務(wù),需要的朋友可以參考下2023-08-08
java實(shí)現(xiàn)簡(jiǎn)單的猜數(shù)字小游戲
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)簡(jiǎn)單猜數(shù)字小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-03-03
intellij idea修改maven配置時(shí)總是恢復(fù)默認(rèn)配置的解決方法idea版本(2020.2.x)
這篇文章主要介紹了intellij idea修改maven配置時(shí)總是恢復(fù)默認(rèn)配置的解決方法idea版本(2020.2.x),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08
Java+MySQL實(shí)現(xiàn)圖書(shū)管理系統(tǒng)(完整代碼)
這篇文章主要介紹了Java+MySQL實(shí)現(xiàn)圖書(shū)管理系統(tǒng)(完整代碼),本文給大家介紹的非常想詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01
SpringBoot利用Validation包實(shí)現(xiàn)高效參數(shù)校驗(yàn)
如果不進(jìn)行校驗(yàn)就直接使用這些數(shù)據(jù),可能會(huì)導(dǎo)致各種問(wèn)題,那么SpringBoot如何利用Validation包實(shí)現(xiàn)高效參數(shù)校驗(yàn)?zāi)?下面讓我們一起來(lái)探討這個(gè)重要的話(huà)題吧2025-04-04
Java編程實(shí)現(xiàn)基于用戶(hù)的協(xié)同過(guò)濾推薦算法代碼示例
這篇文章主要介紹了Java編程實(shí)現(xiàn)基于用戶(hù)的協(xié)同過(guò)濾推薦算法代碼示例,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11
java組件smartupload實(shí)現(xiàn)上傳文件功能
這篇文章主要為大家詳細(xì)介紹了java組件smartupload實(shí)現(xiàn)上傳文件功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10

