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 项目。
相关内链推荐:
- 在 IDEA 中配置 Lombok(Spring Boot 项目必备)
- Logback 日志框架详解:Spring Boot 必备的日志管理工具
- 什么是 .yml 格式?它和 .properties 有什么区别?
- Spring Boot 多环境配置:dev / test / prod 与 profile 激活
SEO 优化建议:
- 标题包含核心关键词:“Spring Boot AOP”、“API 请求日志切面”、“自定义注解”。
- 关键词密度适中:AOP、日志切面、Spring Boot、@ApiOperationLog。
- 建议添加更多代码截图,提升用户动手能力。