💥💥✈️✈️欢迎阅读本文章❤️❤️💥💥
🏆本篇文章阅读大约耗时6分钟。
⛳️****motto**:不积跬步、无以千里**
📋📋📋++本文目录如下:++🎁🎁🎁
目录
[2、配置 logback ,配置 logback.xml 文件](#2、配置 logback ,配置 logback.xml 文件)
[3、yml 文件配置](#3、yml 文件配置)
[1.1 ThreadMdcUtil.java](#1.1 ThreadMdcUtil.java)
[1.2 自定义线程池](#1.2 自定义线程池)
[1.3 自定义线程池注册到容器](#1.3 自定义线程池注册到容器)
[1.4 测试](#1.4 测试)

前言
小伙伴们大家好,上篇文章是本地实操如使用IDEA时,如何自动补充类注释和自定义方法注释,文章链接如下:
【IDEA】✈️自定义模板,自动生成类和方法注释-CSDN博客
本篇文章模拟下项目中如何根据 TraceId 进行日志链路追踪,根据 traceId 关键字,在服务运行日志里面再去查找整个业务记录就比较容易了,参考了网上的很多方法,本地具体使用如下:
(整体实现和测试按照简单来的)


实现
1、引入依赖
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
2、配置 logback ,配置 logback.xml 文件
(配置文件放在 resources 目录下即可)
这里的配置比较简单,以下几个地方着重看下:
日志文件的存储位置: <property name="log" value="C:/Data/extFiles/log" />
[traceId:%X{traceId}]:
搭配代码中的工具使用,位于拦截器中的MDC.put(traceId,tid)
XML
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--日志存储路径-->
<property name="log" value="C:/Data/extFiles/log" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--输出格式化-->
<pattern>[traceId:%X{traceId}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按天生成日志文件 -->
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件名-->
<FileNamePattern>${log}/%d{yyyy-MM-dd}.log</FileNamePattern>
<!--保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>[traceId:%X{traceId}] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="console" />
<appender-ref ref="file" />
</root>
</configuration>

3、yml 文件配置
指定使用的配置文件的具体位置,classpath 一般对应的就是 resources 目录下
XML
logging:
config: classpath:logback.xml
4、自定义日志拦截器
为每次链路请求,添加最终的 traceId
java
import org.slf4j.MDC;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* @author benbenhuang
* @date 2025年04月13日 19:18
* 日志拦截器
*/
@Component
public class LogInterceptor implements HandlerInterceptor {
private static final String traceId = "traceId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String tid = UUID.randomUUID().toString().replace("-", "");
//可以考虑让客户端传入链路ID,但需保证一定的复杂度唯一性;如果没使用默认UUID自动生成
if (!StringUtils.isEmpty(request.getHeader("traceId"))) {
tid = request.getHeader("traceId");
}
MDC.put(traceId, tid);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) {
// 请求处理完成后,清除MDC中的traceId,以免造成内存泄漏
MDC.remove(traceId);
}
}
5、拦截器注册生效
继承这个 WebMvc 配置类,重写添加拦截器方法,将自定义的拦截器的实例注册生效
java
import javax.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
/**
* @author benbenhuang
* @date 2025年04月13日 19:22
*/
@Configuration
public class WebConfigurerAdapter extends WebMvcConfigurationSupport {
@Resource
private LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logInterceptor)
//可以具体制定哪些需要拦截,哪些不拦截,其实也可以使用自定义注解更灵活完成
.addPathPatterns("/**");
}
}
6、测试
简单的请求触发下日志即可,控台和对应的日志文件中信息一致,如下:


特殊场景
1、异步场景

使用异步线程的场景,加入这个调用里面。再次执行看开效果,会发现显然子线程丢失了 traceId。所以需要针对子线程使用情形,做调整,将父线程的 traceId 传递下去给子线程即可。

1.1 ThreadMdcUtil.java
工具类,父线程像线程池中提交任务时,将自身数据传递给子线程
java
import org.slf4j.MDC;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
/**
* @author benbenhuang
* @date 2025年04月13日 20:26
*/
public final class ThreadMdcUtil {
private static final String traceId = "traceId";
// 获取唯一性标识
public static String generateTraceId() {
return UUID.randomUUID().toString().replace("-", "");
}
public static void setTraceIdIfAbsent() {
if (MDC.get(traceId) == null) {
MDC.put(traceId, generateTraceId());
}
}
/**
* 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
*
* @param callable
* @param context
* @param <T>
* @return
*/
public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
return callable.call();
} finally {
MDC.clear();
}
};
}
/**
* 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
*
* @param runnable
* @param context
* @return
*/
public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setTraceIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}
1.2 自定义线程池
重写部分方法,使用工具类将父线程部分信息,传递到子线程
java
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
/**
* @author benbenhuang
* @date 2025年04月13日 20:28
*/
public final class TestThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
public TestThreadPoolTaskExecutor() {
super();
}
@Override
public void execute(Runnable task) {
super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
}
1.3 自定义线程池注册到容器
将自定义的线程池注册为 bean 实例,交由 spring 管理,这里指定了 bean 实例名称,防止名称与容器中的原有 bean 冲突
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* @author benbenhuang
* @date 2025年04月13日 20:29
*/
@EnableAsync
@Configuration
public class ThreadPoolConfig {
/**
* 声明一个线程池
*/
@Bean("devTaskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
TestThreadPoolTaskExecutor executor = new TestThreadPoolTaskExecutor();
//核心线程数5:线程池创建时候初始化的线程数
executor.setCorePoolSize(5);
//最大线程数5:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
executor.setMaxPoolSize(5);
//缓冲队列500:用来缓冲执行任务的队列
executor.setQueueCapacity(500);
//允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
executor.setKeepAliveSeconds(60);
//线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
executor.setThreadNamePrefix("taskExecutor-");
executor.initialize();
return executor;
}
}
1.4 测试
结果如下,子线程的日志也有对应的 traceId 参数:

章末
跨服务也可以使用,仔细看拦截器这里,traceId,会取用上级传递过来的(如果有),也就是上游服务的请求中带上当前的 traceId,即可传递到下游服务,将整个链路串起来
或者使用三方工具,比如 skywalking

文章到这里就结束了~

往期推荐 > > >
【接口负载】✈️整合 Resilience4j 指定接口负载,避免过载