SpringBoot 项目添加 MDC 日志链路追踪的执行流程
目录
1. 线程池配置
2. 拦截器配置
3. 日志文件配置
4. 使用方法示例
4.1. 异步使用
4.2. 定时任务
日志链路追踪的意思就是将一个标志跨线程进行传递,在一般的小项目中也就是在你新起一个线程的时候,或者使用线程池执行任务的时候会用到,比如追踪一个用户请求的完整执行流程。
这里用到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 日志链路追踪内容请搜索无名以前的文章或继续浏览下面的相关文章希望大家以后多多支持无名!
同类资源
- Hook的学习例子,hook计算器
Hook的例子本文件感兴趣的可以参考一下。...
- delphi经纬度在范围判断
delphi经纬度在范围判断本文件感兴趣的可以参考一下。...
- delphi实现ftp上传下载文件到客户端
delphi实现ftp上传下载文件到客户端本文件感兴趣的可以参考一下。...
- Pascal局域网共享资源代码
Pascal局域网共享资源代码本文件感兴趣的可以参考一下,带源代码的局域网共享资源浏览器。...
- 远程控制实例代码,可查看并控制远程桌面
远程控制实例代码,可查看并控制远程桌面本文件感兴趣的可以参考一下。...
- delpho OCR图像识别
delphoOCR图像识别实例代码本文件感兴趣的可以参考一下。...
- 自画TListBox界面
自画TListBox界面例子源代码,TListBox显示图片与文字列表。...
- AMENcron定时任务模块(1.1.3)
AMENcron定时任务模块本文件感兴趣的可以参考一下,cron获取列表错误,导致不能准确定时并执行任务,一般是跨月...
- 窗口组件自适应模块1.1demo源码
窗口组件自适应模块1.1demo源码本文件感兴趣的可以参考一下,理论上应该是无线层级,如果使用上有任何问题。...
- XML模块,XML调用和构造
XML模块,XML调用和构造本文件感兴趣的可以参考一下,最近闲来无事写的模块。...
- HP_Socket-5.8.6中文/英文模块/支持库
HP_Socket-5.8.6中文/英文模块/支持库本文件感兴趣的可以参考一下,由于易语言本身核心库的代码太老了一些...
- zyJson模块含源码
zyJson模块含源码本文件感兴趣的可以参考一下,3.2.4多了一个zyJsonDocument,解析创建json就用这个,zyJsonVal...