springboot实现日志追踪

一、MDC介绍

​ MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

二、API说明

  • clear() => 移除所有MDC
  • get (String key) => 获取当前线程MDC中指定key的值
  • getContext() => 获取当前线程MDC的MDC
  • put(String key, Object o) => 往当前线程的MDC中存入指定的键值对
  • remove(String key) => 删除当前线程MDC中指定的键值对

三、优点

  • 代码简洁,日志风格统一,不需要在log打印中手动拼写traceId,即LOGGER.info("traceId:{} ", traceId)

四、代码实现

4.1 请求没有子线程
4.1.1 实现日志拦截器
@Component
public class LogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    System.out.println("进入拦截器");
    //如果有上层调用就用上层的ID
    String traceId = request.getHeader(Constants.TRACE_ID);
    if (traceId == null) {
        traceId = TraceIdUtil.getTraceId();
    }

    MDC.put(Constants.TRACE_ID, traceId);
    return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
    throws Exception {
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
    throws Exception {
    //调用结束后删除
    System.out.println("调用结束");
    MDC.remove(Constants.TRACE_ID);
}
}
4.2.2 注册拦截器
@Configuration
public class MvcInterceptorConfig extends WebMvcConfigurationSupport {
@Autowired
private LogInterceptor logInterceptor;

@Override
protected void addInterceptors(InterceptorRegistry registry) {
    // 多个拦截器组成一个拦截器链
    // addPathPatterns 用于添加拦截规则,/**表示拦截所有请求
    // excludePathPatterns 用户排除拦截
    registry.addInterceptor(logInterceptor).addPathPatterns("/**");
    super.addInterceptors(registry);
}
}
4.3.3 日志输出格式设置
<property name="LOG_PATTERN_CONSOLE">
	[TRACEID:%X{traceId}] %clr{%d{HH:mm:ss.SSS}}{faint} %clr{%5p} %clr{[%15.15t]}{faint} %clr{%C{1.}(%M:%L)}{cyan} %clr{:}{faint} %m%n%xwEx
</property>

需要注意%X{traceId}中的threadId 需要和日志拦截中MDC put的key是一样的

4.2 请求有子线程的情况

​ slf4j的MDC机制其内部基于ThreadLocal实现。所以我们调用 MDC.put()方法传入的请求ID只在当前线程有效。所以,主线程中设置的MDC数据,在其子线程(线程池)中是无法获取的。

官方建议

1)在父线程新建子线程之前调用MDC.getCopyOfContextMap()方法将MDC内容取出来传给子线程

2)子线程在执行操作前先调用MDC.setContextMap()方法将父线程的MDC内容设置到子线程

代码实现:

1)使用Aop拦截请求,与上面相同

2)log4j日志配置与上面相同

3)装饰器模式装饰子线程,有两种方式

4.2.1 实现方式1——装饰Runnable

​ 使用装饰器模式,对Runnable接口进行一层装饰,在创建MDCRunnable类对Runnable接口进行一层装饰。

在创建MDCRunnable类时保存当前线程的MDC值,再执行run()方法

public class MDCRunnable implements Runnable{

private Runnable runnable;

/**
 * 保存当前主线程的MDC值
 */
private final Map<String, String> mainMdcMap;

public MDCRunnable(Runnable runnable) {
    this.runnable = runnable;
    this.mainMdcMap = MDC.getCopyOfContextMap();
}

@Override
public void run() {
    // 将父线程的MDC值赋给子线程
    for (Map.Entry<String, String> entry : mainMdcMap.entrySet()) {
        MDC.put(entry.getKey(), entry.getValue());
    }
    // 执行被装饰的线程run方法
    runnable.run();
    // 执行结束移除MDC值
    for (Map.Entry<String, String> entry : mainMdcMap.entrySet()) {
       MDC.remove(entry.getKey());
    }
  }
}

测试:使用MDCRunnable代替Runnable

@PostMapping("/test")
public String test(@RequestBody String str){
    log.info("进入test请求方法");
    SmsThreadPoolUtil.getInstance().execute(new MDCRunnable(new Runnable() {
        @Override
        public void run() {
            log.info("子线程1执行任务");
        }
    }));
    executorService.execute(new MDCRunnable(new Runnable() {
        @Override
        public void run() {
            log.info("子线程2执行任务");
        }
    }));

    log.info("test方法执行结束");
    return str;
}
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.857 DEBUG [nio-9999-exec-1] o.s.c.l.LogFormatUtils(traceDebug:91) : Read "application/json;charset=UTF-8" to ["{
    "str":"11111111"
}"]
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.863  INFO [nio-9999-exec-1] c.t.c.TestController(test:41) : 进入test请求方法
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.865  INFO [pool-3-thread-1] c.t.c.TestController$1(run:45) : 子线程1执行任务
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.865  INFO [nio-9999-exec-1] c.t.c.TestController(test:55) : test方法执行结束
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.865  INFO [pool-2-thread-1] c.t.c.TestController$2(run:51) : 子线程2执行任务
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.871 DEBUG [nio-9999-exec-1] o.s.w.s.m.m.a.AbstractMessageConverterMethodProcessor(writeWithMessageConverters:268) : Using 'text/plain', given [*/*] and supported [text/plain, */*, application/json, application/*+json]
[TRACEID:4dead5ddd1a34d10a19ae286dea56ab3] 16:24:20.872 DEBUG [nio-9999-exec-1] o.s.c.l.LogFormatUtils(traceDebug:91) : Writing ["{
    "str":"11111111"
}"]
4.2.2 实现方式2——装饰线程池
public class MDCThreadPoolExecutor extends ThreadPoolExecutor {
public MDCThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}

public MDCThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                    BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}

public MDCThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                    BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}

public MDCThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                    BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
                                    RejectedExecutionHandler handler) {
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}

@Override
public void execute(final Runnable runnable) {
    // 获取父线程MDC中的内容,必须在run方法之前,否则等异步线程执行的时候有可能MDC里面的值已经被清空了,这个时候就会返回null
    final Map<String, String> context = MDC.getCopyOfContextMap();
    super.execute(new Runnable() {
        @Override
        public void run() {
            // 将父线程的MDC内容传给子线程
            MDC.setContextMap(context);
            try {
                // 执行异步操作
                runnable.run();
            } finally {
                // 清空MDC内容
                MDC.clear();
            }
        }
    });
  }
}

测试:用MDCThreadPoolExecutor 代替ThreadPoolExecutor

public class SmsThreadPoolUtil {
	//定义类型
	private static ThreadPoolExecutor pool=new MDCThreadPoolExecutor(10, 20,
			10, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(10000), new RejectedPoolUtils());
	
	public static ThreadPoolExecutor getInstance() {
		return pool;
	}
}

@Service
@Slf4j
public class RejectedPoolUtils implements RejectedExecutionHandler{
	@Override
	public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
		log.info("队列达到最大数");
	}
}
@PostMapping("/test")
public String test(@RequestBody String str){
    log.info("进入test请求方法");
    SmsThreadPoolUtil.getInstance().execute(new Runnable() {
        @Override
        public void run() {
            log.info("子线程1执行任务");
        }
    });
    log.info("test方法执行结束");
    return str;
}
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值