SpringBoot|SpringBoot 项目添加 MDC 日志链路追踪的执行流程
目录
- 1. 线程池配置
- 2. 拦截器配置
- 3. 日志文件配置
- 4. 使用方法示例
- 4.1. 异步使用
- 4.2. 定时任务
这里用到
MDC
和ThreadLocal
,分别由下面的包提供:java.lang.ThreadLocalorg.slf4j.MDC
直接上代码:
1. 线程池配置 如果你直接通过手动新建线程来执行异步任务,想要实现标志传递的话,需要自己去实现,其实和线程池一样,也是调用
MDC
的相关方法,如下所示://取出父线程的MDCMap 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 */@Slf4jpublic class MdcTaskExecutor extends ThreadPoolTaskExecutor {@OverridepublicFuture submit(Callable task) {log.info("mdc thread pool task executor submit"); Map context = MDC.getCopyOfContextMap(); return super.submit(() -> {T result; if (context != null) {//将父线程的MDC内容传给子线程MDC.setContextMap(context); } else {//直接给子线程设置MDCMDC.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; }); }@Overridepublic void execute(Runnable task) {log.info("mdc thread pool task executor execute"); Map context = MDC.getCopyOfContextMap(); super.execute(() -> {if (context != null) {//将父线程的MDC内容传给子线程MDC.setContextMap(context); } else {//直接给子线程设置MDCMDC.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@Configurationpublic class ThreadPoolConfig {/*** 异步任务线程池* 用于执行普通的异步请求,带有请求链路的MDC标志*/@Beanpublic 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* 和上面的线程池没啥区别,只是名字不同*/@Beanpublic 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@Componentpublic class LogInterceptor implements HandlerInterceptor {@Overridepublic 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; }@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {//log.info("执行 LogInterceptor"); }@Overridepublic 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 */@Configurationpublic class WebMvcConfig implements WebMvcConfigurer {@Autowiredprivate LogInterceptor logInterceptor; /*** 拦截器注册*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(logInterceptor); }}
3. 日志文件配置 需要在
logback-spring.xml
文件中的日志打印格式里添加%X{trace_id}
,如下所示:%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{trace_id}] [%level] [%thread] [%class:%line] - %m%n UTF-8
4. 使用方法示例
4.1. 异步使用
这里注意,异步方法的调用不能直接调用当前类的方法,也就是说调用方法和异步方法不能在同一个类里,否则会变为同步执行。
/*** 异步方法*///@Async//这种写法,当只有一个线程池时,会使用该线程池执行,有多个则会使用SimpleAsyncTaskExecutor@Async(value = "https://www.it610.com/article/commonThreadPool")//指定执行的线程池@Overridepublic 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@Componentpublic class TestTimeTask {//基于注解@Scheduled默认为单线程,开启多个任务时,任务的执行时机会受上一个任务执行时间的影响。//使用的线程池是taskScheduler,线程ID为scheduling-x//添加@Async注解指定线程池,则可以多线程执行定时任务(原本是单线程的)。/*** 两次任务开始的时间间隔为2S* 不使用线程池,单线程间隔则为4S。单线程保证不了这个2S间隔,因为任务执行耗时超过了定时间隔,就会影响下一次任务的执行* 使用线程池,多线程执行,时间间隔为2S*///@Async(value = "https://www.it610.com/article/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 = "https://www.it610.com/article/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 日志链路追踪内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
推荐阅读
- Activiti(一)SpringBoot2集成Activiti6
- SpringBoot调用公共模块的自定义注解失效的解决
- 解决SpringBoot引用别的模块无法注入的问题
- 17|17 关山松 第二课作业#公众号项目# D20
- RxJava|RxJava 在Android项目中的使用(一)
- 编写字典程序
- Hacking|Hacking with iOS: SwiftUI Edition - SnowSeeker 项目(一)
- 靠QQ月入上万灰色暴利偏门的项目
- spring|spring boot项目启动websocket
- springboot使用redis缓存