您好,登录后才能下订单哦!
这篇文章主要介绍“SpringBoot 项目怎么添加 MDC 日志链路追踪”,在日常操作中,相信很多人在SpringBoot 项目怎么添加 MDC 日志链路追踪问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”SpringBoot 项目怎么添加 MDC 日志链路追踪”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
1. 线程池配置
2. 拦截器配置
3. 日志文件配置
4. 使用方法示例
4.1. 异步使用
4.2. 定时任务
日志链路追踪的意思就是将一个标志跨线程进行传递,在一般的小项目中也就是在你新起一个线程的时候,或者使用线程池执行任务的时候会用到,比如追踪一个用户请求的完整执行流程。
这里用到MDC
和ThreadLocal
,分别由下面的包提供:
java.lang.ThreadLocal org.slf4j.MDC
直接上代码:
如果你直接通过手动新建线程来执行异步任务,想要实现标志传递的话,需要自己去实现,其实和线程池一样,也是调用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; } }
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); } }
需要在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>
这里注意,异步方法的调用不能直接调用当前类的方法,也就是说调用方法和异步方法不能在同一个类里,否则会变为同步执行。
/** * 异步方法 */ //@Async//这种写法,当只有一个线程池时,会使用该线程池执行,有多个则会使用SimpleAsyncTaskExecutor @Async(value = "commonThreadPool")//指定执行的线程池 @Override public void async() { log.info("测试异步线程池"); }
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 日志链路追踪”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。