日志链路追踪的意思就是将一个标志跨线程进行传递,在一般的小项目中也就是在你新起一个线程的时候,或者使用线程池执行任务的时候会用到,比如追踪一个用户请求的完整执行流程。
这里用到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;
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;
@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;
@Slf4j
@Configuration
public class ThreadPoolConfig {
@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;
}
@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;
@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;
@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;
@Slf4j
@Component
public class TestTimeTask {
//基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。
//使用的线程池是taskScheduler,线程ID为scheduling-x
//添加@Async注解指定线程池,则可以多线程执行定时任务(原本是单线程的)。
//@Async(value = "scheduleThreadPool")
//@Scheduled(fixedRate = 2000)
public void fixedRate() {
log.info("定时间隔任务 fixedRate = {}", LocalDateTime.now());
try {
Thread.sleep(4_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//@Scheduled(fixedDelay = 2_000)
public void fixedDelay() {
log.info("延迟定时间隔任务 fixedDelay = {}", LocalDateTime.now());
try {
Thread.sleep(4_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//@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 日志链路追踪内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!