1. 为什么你的SpringBoot项目急需一个“日志身份证”?
你有没有遇到过这样的场景?线上系统突然报错,用户甩过来一张截图,上面只有一句“系统异常,请稍后再试”。你看着茫茫多的日志文件,心里只有一个念头:这到底是哪个用户、哪次请求、经过了哪些服务模块导致的?如果日志里能有一个像“身份证号”一样唯一的标识,把所有相关的日志条目都串起来,那该多好。
这个“身份证号”,在技术圈里我们通常叫它 traceId,也就是追踪ID。它的作用,简单来说,就是给单次请求的完整执行链路打上一个唯一的标签。无论这个请求在你的SpringBoot应用里调用了多少方法、经过了哪些组件、打印了多少行日志,只要它们共享同一个traceId,你就能像串珍珠一样,把它们从海量日志中快速、精准地挑出来。
你可能会想,这不是分布式链路追踪(比如SkyWalking、Zipkin)才需要的东西吗?我的项目就是个单体应用,有必要这么麻烦吗?根据我多年的经验,太有必要了。即使是单体应用,一个外部请求进来,也可能触发一连串的Service调用、数据库操作、缓存查询、消息发送。当并发量上来,日志交错打印时,没有traceId,排查问题就像在闹市中找一个没穿特定衣服的人,效率极低。而有了traceId,你就能瞬间定位到“那个穿红色上衣、蓝色裤子的人”的所有行动轨迹。
实现这个功能的核心,就是MDC。MDC,全称Mapped Diagnostic Context,翻译过来叫“映射诊断上下文”,是SLF4J提供的一个非常好用的工具。你可以把它理解为一个线程绑定的、键值对形式的临时储物柜。在一个请求的生命周期内(通常对应一个线程),你可以把traceId存进去,之后在这个线程里任何地方打印日志,日志框架都能自动从这个“储物柜”里取出traceId,并把它织入到日志文本中。这样一来,你无需修改任何业务代码的日志打印语句,就能实现全链路日志的自动标记。
下面,我就手把手带你,从零开始,在SpringBoot项目中利用MDC打造一套高效、整洁的日志链路追踪方案。这套方案不仅适用于单体应用,也是你日后理解复杂分布式追踪的基石。
2. 5分钟上手:创建你的第一个TraceId过滤器
万事开头难?在这里一点也不难。给请求加traceId,最核心、最优雅的方式就是利用Servlet Filter。Filter是请求进入Spring MVC控制器之前的门户,在这里生成和设置traceId,能确保请求在后续所有处理环节中都能携带这个标识。
2.1 编写TraceId过滤器
我们不造轮子,直接继承Spring提供的OncePerRequestFilter,它能保证一次请求只经过这个过滤器一次,避免在一些转发(Forward)场景下被重复执行。
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.UUID;
@Component
public class TraceIdFilter extends OncePerRequestFilter {
// 定义在MDC中存储traceId的键名,全局统一就用这个
private static final String TRACE_ID_KEY = "traceId";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 1. 尝试从请求头中获取traceId(为了兼容上游服务或前端已传递的情况)
String traceId = request.getHeader(TRACE_ID_KEY);
// 2. 如果请求头中没有,则自己生成一个唯一的ID
if (traceId == null || traceId.isEmpty()) {
// 使用UUID生成,简单够用。也可以考虑更精简的分布式ID算法,如Snowflake
traceId = UUID.randomUUID().toString().replace("-", "").toLowerCase();
}
// 3. 将traceId放入MDC这个“线程储物柜”
MDC.put(TRACE_ID_KEY, traceId);
try {
// 可选:为了方便前端或下游服务追踪,可以将traceId设置到响应头中
response.setHeader(TRACE_ID_KEY, traceId);
// 4. 放行请求,进入后续的控制器、服务等
filterChain.doFilter(request, response);
} finally {
// 5. 关键一步:请求结束后,务必清理MDC!
// 因为Tomcat等Web服务器使用的是线程池,线程会被复用。
// 如果不清理,下一个处理请求的线程会错误地携带上一个请求的traceId。
MDC.remove(TRACE_ID_KEY);
}
}
}
这段代码的逻辑非常清晰,就像一条流水线:检查 -> 生成/获取 -> 存入 -> 传递 -> 清理。我特别想强调finally块中的MDC.remove(TRACE_ID_KEY);。这是我早期踩过的一个坑,当时没清理,在压测时发现日志完全乱套了,A请求的日志打在了B请求的traceId下。所以,“有借有还” 是使用MDC的第一准则。
2.2 进阶思考:如何生成更专业的TraceId?
上面的例子我们用UUID,它保证了唯一性,但字符串较长(32位),在日志中略显臃肿。在实际生产环境中,我们可能会追求更短、更有语义的ID。这里给你几个思路:
- 精简UUID:去掉连字符,如上例,得到32位十六进制字符串。
- 时间戳+随机数:例如
1700000000000_1234,前半部分是毫秒时间戳,便于按时间排序,后半部分是随机数。 - 分布式ID算法:如Snowflake算法生成的ID,是数字类型,存储和传输效率更高,且带有时间戳和机器ID信息。你可以引入
Hutool等工具库来简化操作。 - 继承与传递:如果你的系统是微服务架构的一部分,那么
traceId通常由网关或第一个接收请求的服务生成,并通过HTTP头(如X-Trace-Id)传递给下游所有服务。我们的过滤器优先从请求头读取,就是为了完美支持这种场景。
你可以根据项目的复杂度和团队规范,选择最适合的生成策略。对于大多数单体或初创项目,精简版的UUID已经完全够用。
3. 让日志开口说话:配置Logback输出TraceId
traceId已经放进了MDC,但如果日志输出格式里没有指定要打印它,那它就是个“隐形人”。接下来,我们需要配置日志框架,告诉它:“打印日志时,请把MDC里traceId这个键对应的值也带上”。
SpringBoot默认使用Logback,我们就以配置logback-spring.xml为例。这个文件需要放在src/main/resources目录下。
3.1 基础配置:在日志模式中加入TraceId
首先,来看一个最简洁、最核心的配置片段:
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 注意这个pattern -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId}] - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>

1578

被折叠的 条评论
为什么被折叠?



