1453 字
约 4 分钟
1
Spring Boot AOP 实现 API 请求日志切面(自定义注解实战)

Spring Boot AOP 实现 API 请求日志切面(自定义注解实战)

在后端业务中,对每次请求的入参、被请求类、方法,以及出参、执行耗时等信息进行日志打印,是很有必要的。有了这些信息,当某个接口出现问题时,可以帮助我们快速完成问题的追踪。

前言

在后端业务中,对每次请求的入参、被请求类、方法,以及出参、执行耗时等信息进行日志打印,是很有必要的。有了这些信息,当某个接口出现问题时,可以帮助我们快速完成问题的追踪。那么,Spring Boot 中要如何实现呢?本小节中,小哈就带着大家通过自定义注解和切面编程(AOP)的方法,轻松实现此功能。

什么是自定义注解 (Custom Annotations)?

Java 注解是从 Java 5 开始引入的,它为我们提供了一种元编程的方法,允许我们在不改变代码逻辑的情况下为代码添加元数据。这些元数据可以在编译时或运行时通过反射被访问。

自定义注解就是用户定义的,用于为代码提供元数据的注解。例如,本小节中自定义的 @ApiOperationLog 注解,它用来表示一个方法在执行时需要被记录日志。

自定义 @ApiOperationLog 注解示例

import java.lang.annotation.*;

/**
 * 自定义 API 操作日志注解
 * 用于标记需要记录操作日志的方法
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ApiOperationLog {

    /**
     * 日志描述
     */
    String value() default "";

    /**
     * 操作类型
     */
    String operationType() default "";

    /**
     * 请求路径
     */
    String path() default "";
}

什么是 AOP (面向切面编程)?

AOP(Aspect-Oriented Programming,面向切面编程)是一个编程范式,它提供了一种能力,让开发者能够模块化跨多个对象的横切关注点(例如日志、事务管理、安全等)。

主要概念包括:

  • 切点 (Pointcuts):定义在哪里应用切面(即在哪里插入横切关注点的代码)。
  • 通知 (Advices):定义在特定切点上要执行的代码。常见的通知类型有:
    • 前置通知 (@Before)
    • 后置通知 (@After)
    • 环绕通知 (@Around)
    • 返回通知 (@AfterReturning)
    • 异常通知 (@AfterThrowing)
  • 切面 (Aspects):切面将切点和通知结合起来,定义了在何处和何时应用特定的逻辑。

通过 AOP,我们可以为所有使用 @ApiOperationLog 注解的方法自动添加日志逻辑,而不需要在每个方法中手动添加。

AOP 实现 API 日志切面的核心代码

1. 日志切面类 ApiOperationLogAspect.java

import org.aspectj.lang.JoinPoint;
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.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Aspect
@Component
public class ApiOperationLogAspect {

    private static final Logger log = LoggerFactory.getLogger(ApiOperationLogAspect.class);
    private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    /**
     * 定义切点:所有标注了 @ApiOperationLog 注解的方法
     */
    @Pointcut("@annotation(com.clxhuhhr.aspect.ApiOperationLog)")
    public void apiOperationLogPointCut() {
    }

    /**
     * 环绕通知:记录请求入参、方法执行前后的完整日志
     */
    @Around("apiOperationLogPointCut()")
    public Object aroundApiOperationLog(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取请求信息
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String ip = request.getRemoteAddr();
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getName();
        String methodSignature = joinPoint.getSignature().toLongString();

        // 获取注解信息
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        ApiOperationLog logAnnotation = method.getAnnotation(ApiOperationLog.class);

        String description = logAnnotation == null ? "未标注" : logAnnotation.value();
        String operationType = logAnnotation == null ? "未知" : logAnnotation.operationType();
        String path = logAnnotation == null ? request.getRequestURI() : logAnnotation.path();

        // 打印请求入参
        Object[] args = joinPoint.getArgs();
        String paramStr = this.argsToString(args);

        long startTime = System.currentTimeMillis();
        log.info("=== API 请求日志开始 ===");
        log.info("请求IP: {}", ip);
        log.info("请求路径: {}", path);
        log.info("请求方法: {}#{}", className, methodName);
        log.info("请求参数: {}", paramStr);
        log.info("操作描述: {} | 类型: {} | 描述: {}", description, operationType, path);

        // 执行原方法
        Object result;
        try {
            result = joinPoint.proceed();
        } catch (Exception e) {
            long endTime = System.currentTimeMillis();
            long executeTime = endTime - startTime;
            log.error("=== API 请求异常结束 ===");
            log.error("请求IP: {}", ip);
            log.error("请求路径: {}", path);
            log.error("异常信息: {}", e.getMessage());
            log.error("执行耗时: {}ms", executeTime);
            throw e;
        }

        // 打印执行结果
        long endTime = System.currentTimeMillis();
        long executeTime = endTime - startTime;

        // 打印成功日志
        log.info("=== API 请求日志结束 ===");
        log.info("请求IP: {}", ip);
        log.info("请求路径: {}", path);
        log.info("执行耗时: {}ms", executeTime);
        log.info("返回结果: {}", this.objectToString(result));

        return result;
    }

    /**
     * 对象转字符串
     */
    private String objectToString(Object obj) {
        if (obj == null) {
            return "null";
        }
        return obj.toString();
    }

    /**
     * 参数转字符串
     */
    private String argsToString(Object[] args) {
        if (args == null || args.length == 0) {
            return "无参数";
        }
        StringBuilder sb = new StringBuilder();
        for (Object arg : args) {
            sb.append(arg).append(", ");
        }
        return sb.substring(0, sb.length() - 2);
    }
}

2. Spring Boot 配置

application.yml 中添加以下配置(可选):

logging:
  level:
    com.clxhuhhr.aspect: DEBUG   # 开启 AOP 日志级别

总结

通过自定义 @ApiOperationLog 注解和 AOP 切面,我们可以轻松实现 API 请求日志的统一管理。无需在每个接口方法中手动添加日志代码,真正做到“零入侵”。

这种方式不仅提高了开发效率,还能带来更全面、更友好的日志体验。适用于任何需要记录操作日志的 Spring Boot 项目。

相关内链推荐

SEO 优化建议

  • 标题包含核心关键词:“Spring Boot AOP”、“API 请求日志切面”、“自定义注解”。
  • 关键词密度适中:AOP、日志切面、Spring Boot、@ApiOperationLog。
  • 建议添加更多代码截图,提升用户动手能力。
Spring Boot AOP 实现 API 请求日志切面(自定义注解实战)
http://clxhxhhr.top/posts/325/
作者
clxstart
发布于
2026-08-21
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。