SpringBoot中全局日志记录与请求链路追踪实战教学

 更新时间:2026年07月29日 09:08:20   作者:(farerboy)  
本文将手把手教你如何使用MDC、AOP和Filter搭建一套生产级全局日志系统,自动记录接口入参出参和耗时,并实现TraceId全链路追踪和敏感信息脱敏,让你的系统可观测性瞬间提升

在微服务和高并发架构下,日志是排查问题的“救命稻草”。本文将讲解如何利用 AOP、MDC 等技术,构建一套包含请求追踪、出入参记录、敏感信息脱敏的全局日志系统。

一、 为什么需要全局日志和链路追踪?

想象一下这样的场景:用户反馈下单失败,客服给你一个订单号。你开始查日志…

  • 如果不带唯一标识:你需要在一堆杂乱的并发请求中找到属于那个用户的那一次请求,犹如大海捞针。
  • 如果接口多:如果涉及多个服务调用,你需要跨机器拼接日志片段。

为了解决这些问题,我们需要:

  1. 链路追踪 (TraceId) :为每个请求生成唯一的 ID,贯穿整个处理流程,串联起所有相关日志。
  2. 全局日志 :自动记录接口入参、出参、耗时,减少手动写 log.info(...) 的工作量。

二、 核心组件:MDC (Mapped Diagnostic Context)

MDC 是 Slf4j 提供的一个机制,它实际上是一个线程安全的 ThreadLocal<Map>。我们可以往里面放入键值对(如 TraceId),然后在 Logback 配置中通过 %X{key} 的格式输出。

1. 生成 TraceId

我们需要一个工具类来管理 TraceId。

public class TraceIdUtil {
    
    private static final String TRACE_ID_KEY = "traceId";

    /**
     * 获取当前线程的 TraceId
     */
    public static String getTraceId() {
        String traceId = MDC.get(TRACE_ID_KEY);
        return StringUtils.isEmpty(traceId) ? "" : traceId;
    }

    /**
     * 设置 TraceId
     */
    public static void setTraceId(String traceId) {
        MDC.put(TRACE_ID_KEY, traceId);
    }

    /**
     * 清除 TraceId (非常重要,防止线程池复用导致数据污染)
     */
    public static void removeTraceId() {
        MDC.remove(TRACE_ID_KEY);
    }
}

2. 通过 Filter 拦截请求并设置 TraceId

在请求进入 Controller 之前,我们通过一个 Filter 来生成并设置 TraceId。

@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 确保最先执行
public class TraceIdFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        try {
            // 尝试从请求头获取 TraceId(适用于网关传递过来的场景)
            String traceId = httpRequest.getHeader("X-Trace-Id");
            if (StringUtils.isEmpty(traceId)) {
                traceId = UUID.randomUUID().toString().replace("-", "");
            }
            TraceIdUtil.setTraceId(traceId);
            
            chain.doFilter(request, response);
        } finally {
            // 务必清理,防止内存泄漏或线程池复用导致的脏数据
            TraceIdUtil.removeTraceId();
        }
    }
}

3. 配置 Logback 输出格式

修改 logback-spring.xml,在 pattern 中加入 %X{traceId}

<configuration>
    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n"/>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

现在,你的日志输出将类似这样:2023-10-01 12:00:00.001 [http-nio-8080-exec-1] [a1b2c3d4e5f6...] INFO c.e.controller.UserController - User created successfully

三、 AOP 实现全局接口日志

虽然 MDC 帮我们串联了日志,但我们还需要知道每个接口的入参、出参和耗时。手动打印太麻烦,使用 AOP (Aspect Oriented Programming) 是最好的选择。

1. 引入依赖与启用 AOP

确保引入了 spring-boot-starter-aop

2. 编写日志切面

我们定义一个切面,拦截所有 Controller 层的 public 方法。

@Slf4j
@Aspect
@Component
public class ControllerLogAspect {

    // 定义切点:拦截 Controller 包下的所有方法
    @Pointcut("execution(public * com.example..controller..*.*(..))")
    public void controllerLog() {}

    @Around("controllerLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        // 获取请求信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        
        // 构建日志对象
        LogInfo logInfo = new LogInfo();
        logInfo.setTraceId(TraceIdUtil.getTraceId());
        logInfo.setMethod(request.getMethod());
        logInfo.setUrl(request.getRequestURI());
        logInfo.setIp(request.getRemoteAddr());
        logInfo.setClassMethod(joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        
        // 获取入参
        Object[] args = joinPoint.getArgs();
        // 注意:过滤掉 HttpServletRequest, HttpServletResponse 等无法序列化的参数
        Object[] logArgs = Arrays.stream(args)
                .filter(arg -> !(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse))
                .toArray();
        logInfo.setArgs(logArgs);
        
        // 打印入参日志
        log.info("Request Info: {}", JSON.toJSONString(logInfo));

        Object result;
        try {
            result = joinPoint.proceed();
        } catch (Exception e) {
            // 记录异常日志(通常由全局异常处理器处理,这里也可以记录一份用于审计)
            log.error("Controller Exception: ", e);
            throw e;
        }
        
        // 计算耗时
        long costTime = System.currentTimeMillis() - startTime;
        logInfo.setCostTime(costTime);
        logInfo.setResult(result);
        
        // 打印出参日志
        log.info("Response Info ({}ms): {}", costTime, JSON.toJSONString(logInfo));
        
        return result;
    }

    @Data
    public static class LogInfo {
        private String traceId;
        private String method;
        private String url;
        private String ip;
        private String classMethod;
        private Object[] args;
        private Object result;
        private Long costTime;
    }
}

四、 敏感信息脱敏

在实际生产环境中,日志里绝对不能出现明文密码、身份证号、手机号等敏感信息。我们可以在序列化(如 Jackson 或 Fastjson)输出日志前进行处理。

自定义脱敏序列化器 (以 Jackson 为例)

public class SensitiveDataSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (StringUtils.isEmpty(value)) {
            gen.writeNull();
            return;
        }
        // 简单的脱敏逻辑:保留前 3 后 4,中间星号
        if (value.length() > 8) {
            gen.writeString(value.substring(0, 3) + "****" + value.substring(value.length() - 4));
        } else {
            gen.writeString("****");
        }
    }
}

使用方式:在 DTO 的敏感字段上添加注解

public class UserDTO {
    
    @JsonSerialize(using = SensitiveDataSerializer.class)
    private String phone;
    
    // ...
}

或者在 AOP 中通过正则替换整个 JSON 字符串后再打印。

五、 最佳实践

  1. 异步日志 (AsyncAppender) :生产环境日志量巨大,同步打印日志会阻塞业务线程。务必在 Logback 配置中使用 AsyncAppender 包装 ConsoleFile Appender。
  2. 线程池透传 MDC :如果你的代码使用了 @Async 或自定义线程池,MDC 的值不会自动传递到子线程。你需要实现 TaskDecoratorThreadPoolTaskExecutor 的包装类,在提交任务时拷贝 MDC 上下文,执行完毕后恢复。
  3. 避免打印大对象 :如果入参包含大文件流(MultipartFile)或巨大的列表,不要直接打印 JSON,会撑爆日志文件。在 AOP 中判断参数类型进行过滤。
  4. 日志级别控制 :不要把所有日志都设为 INFO。开发环境可以是 DEBUG,生产环境建议 INFO 或 WARN,Error 必须报警。

六、 总结

通过 MDC + Filter + AOP 的组合拳,我们实现了:

  1. 全链路追踪:每个请求都有唯一的 TraceId。
  2. 自动审计 :无需修改业务代码,自动记录接口出入参和耗时。
  3. 数据安全 :敏感信息脱敏,避免合规风险。

这套方案是构建生产级 SpringBoot 应用的标配,掌握它将为你的系统可观测性打下坚实基础。

到此这篇关于SpringBoot中全局日志记录与请求链路追踪实战教学的文章就介绍到这了,更多相关SpringBoot全局日志系统内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java实现十秒向MySQL插入百万条数据

    Java实现十秒向MySQL插入百万条数据

    这篇文章主要为大家详细介绍了Java如何实现十秒向MySQL插入百万条数据,文中的示例代码讲解详细,对我们学习或工作有一定借鉴价值,需要的可以参考一下
    2022-11-11
  • springboot对压缩请求的处理方法

    springboot对压缩请求的处理方法

    这篇文章主要介绍了springboot对压缩请求的处理,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-05-05
  • Java使用DSA密钥对生成XML签名的方法

    Java使用DSA密钥对生成XML签名的方法

    这篇文章主要介绍了Java使用DSA密钥对生成XML签名的方法,实例分析了java使用DSA密钥对生成XML签名的技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-03-03
  • 浅谈Java(SpringBoot)基于zookeeper的分布式锁实现

    浅谈Java(SpringBoot)基于zookeeper的分布式锁实现

    这篇文章主要介绍了Java(SpringBoot)基于zookeeper的分布式锁实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-03-03
  • IDEA 格式化SQL代码技巧分享

    IDEA 格式化SQL代码技巧分享

    这篇文章主要介绍了IDEA 格式化SQL代码技巧分享,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • IntelliJ IDEA 常用设置(配置)吐血整理(首次安装必需)

    IntelliJ IDEA 常用设置(配置)吐血整理(首次安装必需)

    这篇文章主要介绍了IntelliJ IDEA 常用设置(配置)吐血整理(首次安装必需),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-06-06
  • Java Socket循环接收数据readLine()阻塞的解决方案

    Java Socket循环接收数据readLine()阻塞的解决方案

    这篇文章主要介绍了Java Socket循环接收数据readLine()阻塞的解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-08-08
  • SpringBoot实现自定义指标监控功能

    SpringBoot实现自定义指标监控功能

    本文主要介绍了SpringBoot实现自定义指标监控功能的实现,,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,感兴趣的小伙伴跟着着小编来一起来学习吧
    2024-01-01
  • Java实现异步执行的8种方式总结

    Java实现异步执行的8种方式总结

    这篇文章主要给大家介绍了关于Java实现异步执行的8种方式,异步编程不会阻塞程序的执行,它将耗时的操作提交给后台线程或其他执行环境,并立即返回,使得程序可以继续执行其他任务,需要的朋友可以参考下
    2023-09-09
  • SpringBoot整合Swagger框架过程解析

    SpringBoot整合Swagger框架过程解析

    这篇文章主要介绍了SpringBoot整合Swagger框架过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-05-05

最新评论