在数字化转型中,选择合适的跨平台开发框架不仅能提高效率,还有助于确保数据安全与合规性。
1298
2023-01-07
SpringBoot 项目添加 MDC 日志链路追踪的执行流程
目录1. 线程池配置2. -配置3. 日志文件配置4. 使用方法示例4.1. 异步使用4.2. 定时任务
日志链路追踪的意思就是将一个标志跨线程进行传递,在一般的小项目中也就是在你新起一个线程的时候,或者使用线程池执行任务的时候会用到,比如追踪一个用户请求的完整执行流程。
这里用到MDC和ThreadLocal,分别由下面的包提供:
java.lang.ThreadLocal
org.slf4j.MDC
直接上代码:
1. 线程池配置
如果你直接通过手动新建线程来执行异步任务,想要实现标志传递的话,需要自己去实现,其实和线程池一样,也是调用MDC的相关方法,如下所示:
//取出父线程的MDC
Map
//将父线程的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
log.info("mdc thread pool task executor submit");
Map
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
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 20cuONjrI21/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.setRejectedExhttp://ecutionHandler(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;
cuONjrI
/**
* -注册
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor);
}
}
3. 日志文件配置
需要在logback-spring.xml文件中的日志打印格式里添加%X{trace_id},如下所示:
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();
}
}
}
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~