Spring Boot 中基于 AOP 的 Controller 统一日志打印方案

0 阅读4分钟

Spring Boot 基于AOP实现Controller接口统一日志打印

在日常后端开发中,接口问题排查效率低是最常见的痛点之一:很多项目没有统一的日志规范,Controller接口缺少请求URL、入参、响应结果、执行耗时等核心日志,导致线上报错、接口异常时无法快速定位问题;如果手动在每个接口中打印日志,又会造成代码冗余、维护成本高,还容易遗漏。

为了解决这个问题,本文基于Spring AOP实现一套无侵入、可配置、灵活扩展的Controller层统一日志打印方案,自动记录接口全量信息,同时支持慢接口监控告警,完全满足开发排查问题的核心需求。

一、方案核心优势

  1. 无侵入性:无需修改任何Controller业务代码,通过切面统一处理日志逻辑
  2. 全量信息:自动打印接口URL、请求方法、入参、响应结果、执行耗时
  3. 安全可靠:过滤Web核心对象,避免序列化异常
  4. 灵活配置:慢接口阈值可动态调整,支持按需开关/裁剪日志
  5. 慢接口监控:自动识别超时接口,以WARN级别告警,助力性能优化
  6. 统一规范:所有接口日志格式一致,便于日志检索和问题排查

二、环境依赖

项目基于Spring Boot开发,只需引入3个核心依赖(Maven):

<!-- Spring AOP 核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- FastJSON 用于参数序列化 -->
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.32</version>
</dependency>
<!-- Lombok 简化日志开发 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

三、核心实现代码

直接创建AOP日志切面类,复制即用,适配绝大多数Spring Boot项目:

package com.xm.kite.ztc.common.aspect;

import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;

/**
 * AOP统一日志处理切面
 * 自动打印Controller层接口:URL、方法名、入参、响应结果、执行耗时
 * 支持慢接口告警配置
 */
@Slf4j
@Aspect
@Component
public class LoggerHandler {

    /**
     * 慢接口阈值(单位:毫秒)
     * 从配置文件读取,默认2000ms(2秒)
     */
    @Value("${method-logger.elapsed-time:2000}")
    private Long loggerHandlerElapsedTimeMs;

    /**
     * 切点:匹配指定包下所有Controller的所有方法
     * 可根据项目包路径灵活修改!!!
     */
    @Pointcut("execution(* com.xm.kite.ztc..controller..*.*(..))")
    public void allControllerMethods() {
    }

    /**
     * 环绕通知:包裹目标接口,执行前后统一处理日志
     */
    @Around("allControllerMethods()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 记录接口开始时间
        long startTime = System.currentTimeMillis();

        // 2. 获取Http请求对象,解析请求URL
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String requestUrl = request.getRequestURL().toString();

        // 3. 获取当前接口的类、方法名
        Logger currentLogger = LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringType());
        String methodName = joinPoint.getSignature().getName();

        // 4. 安全处理入参:过滤HttpServletRequest/Response,避免序列化异常
        List<Object> safeArgs = new ArrayList<>();
        for (Object arg : joinPoint.getArgs()) {
            if (arg instanceof HttpServletRequest || arg instanceof HttpServletResponse) {
                safeArgs.add("[" + arg.getClass().getSimpleName() + "]");
            } else {
                safeArgs.add(arg);
            }
        }
        // 入参序列化为JSON
        String requestArgs = JSONObject.toJSONString(safeArgs);

        // 5. 打印【请求入参】日志
        currentLogger.info("请求开始 >>> url: {}, method: {}, args: {}", requestUrl, methodName, requestArgs);

        // 6. 执行目标接口方法
        Object result = joinPoint.proceed();

        // 7. 计算接口耗时
        long elapsedTime = System.currentTimeMillis() - startTime;
        String responseResult = JSONObject.toJSONString(result);

        // 8. 分级打印【响应结果】日志:慢接口WARN告警,正常接口INFO打印
        if (elapsedTime > loggerHandlerElapsedTimeMs) {
            currentLogger.warn("请求结束【慢接口告警】>>> 阈值:{}ms, url: {}, method: {}, args: {}, result: {}, 耗时:{}ms",
                    loggerHandlerElapsedTimeMs, requestUrl, methodName, requestArgs, responseResult, elapsedTime);
        } else {
            currentLogger.info("请求结束 >>> url: {}, method: {}, args: {}, result: {}, 耗时:{}ms",
                    requestUrl, methodName, requestArgs, responseResult, elapsedTime);
        }

        return result;
    }
}

四、灵活配置(核心!支持自定义调整)

1. 配置慢接口阈值

application.yml/application.properties动态调整慢接口标准,无需修改代码:

# 日志配置:慢接口阈值(单位:毫秒)
method-logger:
  elapsed-time: 1500  # 调整为1.5秒,根据项目需求修改

2. 灵活调整打印内容(按需改造)

针对不同开发场景,可快速裁剪/扩展日志打印内容,以下是常用扩展方案:

(1)添加请求IP、请求方式
// 获取请求方式
String requestMethod = request.getMethod();
// 获取客户端IP
String clientIp = request.getRemoteAddr();
// 日志中新增打印
currentLogger.info("url: {}, ip: {}, requestType: {}, method: {}, args: {}", 
        requestUrl, clientIp, requestMethod, methodName, requestArgs);
(2)过滤敏感参数(密码、手机号等)
// 改造入参过滤逻辑,隐藏敏感字段
for (Object arg : safeArgs) {
    if (arg instanceof UserDTO) {
        UserDTO user = (UserDTO) arg;
        user.setPassword("***"); // 密码脱敏
    }
}
(3)日志总开关(按需开启/关闭)
// 新增配置开关
@Value("${method-logger.enabled:true}")
private boolean loggerEnabled;

// 环绕通知中判断
@Around("allControllerMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
    if (!loggerEnabled) {
        return joinPoint.proceed(); // 关闭日志,直接执行接口
    }
    // 原有日志逻辑...
}
(4)修改切点范围
// 示例1:只打印指定包下的接口
@Pointcut("execution(* com.xm.kite.ztc.user.controller..*.*(..))")
// 示例2:只打印带有@GetMapping注解的方法
@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
(5)关闭响应结果打印(简化日志)

如果接口返回数据过大,可取消响应结果打印:

// 直接删除result参数,简化日志
currentLogger.info("请求结束 >>> url: {}, 耗时:{}ms", requestUrl, elapsedTime);

五、代码核心解析

  1. 切点定义@Pointcut精准匹配项目中所有Controller接口,是AOP日志的生效范围
  2. 环绕通知@Around是核心,在接口执行前后分别记录请求、响应信息
  3. 参数安全处理:过滤HttpServletRequest/Response对象,避免JSON序列化失败
  4. 日志分级:正常接口用INFO级别,慢接口用WARN级别告警,便于日志筛选
  5. 动态配置:通过@Value注入配置参数,支持环境差异化配置

六、日志效果展示

启动项目后,调用任意Controller接口,控制台会打印标准化日志:

# 正常接口
INFO  c.x.k.z.t.controller.UserController - 请求开始 >>> url: http://localhost:8080/user/get, method: getUser, args: {"id":1}
INFO  c.x.k.z.t.controller.UserController - 请求结束 >>> url: http://localhost:8080/user/get, method: getUser, args: {"id":1}, result: {"code":200,"data":{"name":"张三"}}, 耗时:12ms

# 慢接口告警
WARN  c.x.k.z.t.controller.OrderController - 请求结束【慢接口告警】>>> 阈值:2000ms, url: http://localhost:8080/order/list, method: getOrderList, args: {"page":1}, result: {...}, 耗时:2560ms

示例:

image.png

七、注意事项

  1. AOP生效:Spring Boot 2.x+引入spring-boot-starter-aop后,无需额外配置 @EnableAspectJAutoProxy,自动开启AOP
  2. 序列化问题:如果实体类存在循环引用,FastJSON会自动忽略,无需担心报错
  3. 性能影响:日志打印为轻量级操作,对接口性能无明显损耗
  4. 包路径修改:必须将切点中的包路径com.xm.kite.ztc改为自己项目的实际包路径,否则日志不生效

总结

这套基于Spring AOP的统一日志方案,彻底解决了接口排查无日志、手动打印日志冗余的问题,同时具备极高的灵活性:开发人员可根据需求自由调整打印内容、开关日志、配置慢接口阈值。

方案无侵入、易扩展、标准化,完全适配企业级Spring Boot项目,大幅提升线上问题排查效率和接口性能监控能力。