Spring Boot 实现AOP日志切面全流程教程
AOP(面向切面编程)是Spring框架的核心特性之一,常用于日志记录、权限校验、事务管理等场景。本文将手把手教你如何在Spring Boot项目中实现AOP日志切面功能,包括依赖引入、切点定义、切面实现、注解自定义等内容。
下载地址
效果
切入com.anfioo下的所有controller层
切入com.anfioo下的所有service层

切入自定义注解@LogRecord


全都开启

可以使用aop切片更好的打印这个方法信息,而不污染原有的方法
一、引入依赖
首先,确保你的Spring Boot项目已经引入了AOP相关依赖。Spring Boot Starter通常已经包含了AOP依赖,但你可以在pom.xml
中显式添加:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
二、配置AOP属性(可选)
为了灵活控制切面的开启与关闭,我们可以通过配置文件添加自定义属性。例如:
yaml
# application.yaml
aop:
debug:
controller: true # 控制Controller切面是否开启
service: false # 控制Service切面是否开启
log-record: true # 控制LogRecord注解切面是否开启
并通过@ConfigurationProperties
将其绑定到Java类:
java
@Data
@Component
@ConfigurationProperties(prefix = "aop.debug")
public class AopDebugProperties {
private Boolean controller = true;
private Boolean service = false;
private Boolean logRecord = true;
}
三、定义切点(Pointcut)
切点用于指定哪些类或方法会被AOP拦截。常见的切点表达式有:
execution(public * com.example..controller..*(..))
:拦截所有controller包下的公共方法@annotation(com.example.LogRecord)
:拦截所有被自定义注解标记的方法
四、实现切面(Aspect)
以Controller层日志为例,实现一个切面类:
java
package com.anfioo.common.log;
import com.anfioo.common.bean.AopDebugProperties;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* 全局 Controller 层日志记录切面
*/
@Aspect
@Component
@Slf4j
public class ControllerLogAspect {
@Autowired
private AopDebugProperties aopDebugProperties;
/**
* 定义切点,匹配 com.anfioo 包下所有子包中的 controller 类的公共方法
*/
@Pointcut("execution(public * com.anfioo..controller..*(..))")
public void controllerMethods() {
}
/**
* 环绕通知,用于记录 controller 层方法的请求和响应信息
*
* @param joinPoint 切入点对象,包含被拦截方法的信息
* @return 被拦截方法的执行结果
* @throws Throwable 如果执行过程中出现异常
*/
@Around("controllerMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
if (Boolean.FALSE.equals(aopDebugProperties.getController())) {
// 不开启,直接执行原方法
return joinPoint.proceed();
}
// 记录开始时间
long start = System.currentTimeMillis();
// 获取目标类的 Class 对象
Class<?> targetClass = joinPoint.getTarget().getClass();
// 获取目标类的 Logger 对象
Logger logger = LoggerFactory.getLogger(targetClass);
// 获取当前请求的 HttpServletRequest 对象
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 提取请求的 URL、方法、IP 地址、处理方法和参数
String url = request.getRequestURL().toString();
String method = request.getMethod();
String ip = request.getRemoteAddr();
String classMethod = joinPoint.getSignature().toShortString();
Object[] args = joinPoint.getArgs();
String red = "\u001B[31m"; // ANSI 红色
String line = IntStream.range(0, 200) // 200 可以改成任意长度
.mapToObj(i -> "#")
.collect(Collectors.joining());
System.out.println(red + line);
// 记录请求信息
logger.info("\n====== 🌐 请求信息 ======\n" +
"📌 URL : {}\n" +
"🔁 Method : {}\n" +
"👤 IP : {}\n" +
"🎯 Handler : {}\n" +
"🧾 Parameters : {}\n" +
"==========================",
url, method, ip, classMethod, Arrays.toString(args));
// 尝试执行目标方法,并记录执行结果或异常信息
Object result;
try {
result = joinPoint.proceed();
} catch (Exception e) {
// 记录异常信息,并重新抛出异常
logger.error("\n====== 🌐 ❌ 异常信息 ======\n" +
"🎯 Handler : {}\n" +
"🛑 Error : {}\n" +
"==========================",
classMethod, e.getMessage(), e);
throw e;
}
// 记录结束时间,并计算耗时
long end = System.currentTimeMillis();
// 记录响应信息
logger.info("\n====== 🌐 ✅ 响应信息 ======\n" +
"🎯 Handler : {}\n" +
"📤 Response : {}\n" +
"⏱️ 耗时 : {} ms\n" +
"==========================",
classMethod, result, end - start);
return result;
}
}
说明
@Aspect
:声明该类为切面@Pointcut
:定义切点@Around
:环绕通知,可在方法执行前后插入逻辑ProceedingJoinPoint
:用于获取方法信息、参数、执行目标方法等
五、自定义注解与切面
有时我们希望只对特定方法记录日志,可以自定义注解:
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogRecord {
String value() default "";
}
并实现对应的切面:
java
package com.anfioo.common.log;
import com.anfioo.common.bean.AopDebugProperties;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* 记录 @LogRecord 注解标记的方法调用日志
*/
@Aspect
@Component
@Slf4j
public class LogRecordAspect {
@Autowired
private AopDebugProperties aopDebugProperties;
/**
* 切点:拦截所有带有 @LogRecord 注解的方法
*/
@Pointcut("@annotation(com.anfioo.common.log.LogRecord)")
public void logRecordMethods() {
}
/**
* 环绕通知:记录方法执行详情
*/
@Around("logRecordMethods() && @annotation(logRecord)")
public Object around(ProceedingJoinPoint joinPoint, LogRecord logRecord) throws Throwable {
if (Boolean.FALSE.equals(aopDebugProperties.getLogRecord())) {
return joinPoint.proceed(); // 不开启则跳过
}
long start = System.currentTimeMillis();
Class<?> targetClass = joinPoint.getTarget().getClass();
Logger logger = LoggerFactory.getLogger(targetClass);
String description = logRecord.value(); // 注解中的描述信息
String methodName = joinPoint.getSignature().toShortString();
Object[] args = joinPoint.getArgs();
logger.info("\n====== 📝 LogRecord 方法调用 ======\n" +
"📌 描述 : {}\n" +
"🔧 方法 : {}\n" +
"🧾 参数 : {}\n" +
"=====================================",
description, methodName, Arrays.toString(args));
Object result;
try {
result = joinPoint.proceed();
} catch (Exception e) {
logger.error("\n====== 📝❌ LogRecord 异常 ======\n" +
"🔧 方法 : {}\n" +
"🛑 异常信息 : {}\n" +
"=====================================",
methodName, e.getMessage(), e);
throw e;
}
long end = System.currentTimeMillis();
logger.info("\n====== 📝✅ LogRecord 返回结果 ======\n" +
"🔧 方法 : {}\n" +
"📤 返回值 : {}\n" +
"⏱️ 耗时 : {} ms\n" +
"=====================================",
methodName, result, end - start);
return result;
}
}
六、Service层切面实现
同理,可以为Service层实现切面:
java
package com.anfioo.common.log;
import com.anfioo.common.bean.AopDebugProperties;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* Service 层方法日志切面(记录业务逻辑调用)
*/
@Aspect
@Component
@Slf4j
public class ServiceLogAspect {
@Autowired
private AopDebugProperties aopDebugProperties;
/**
* 切入 com.anfioo 包下所有 service 的方法
*/
@Pointcut("execution(* com.anfioo..service..*(..))")
public void serviceMethods() {
}
/**
* 环绕通知记录 service 层方法的调用情况
*/
@Around("serviceMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
if (Boolean.FALSE.equals(aopDebugProperties.getService())) {
// 不开启,直接执行原方法
return joinPoint.proceed();
}
long start = System.currentTimeMillis();
Class<?> targetClass = joinPoint.getTarget().getClass();
Logger logger = LoggerFactory.getLogger(targetClass);
String methodName = joinPoint.getSignature().toShortString();
Object[] args = joinPoint.getArgs();
logger.info("\n====== ⚙️ Service 方法调用 ======\n" +
"🔧 Method : {}\n" +
"🧾 Parameters : {}\n" +
"===============================",
methodName, Arrays.toString(args));
Object result;
try {
result = joinPoint.proceed();
} catch (Exception e) {
logger.error("\n====== ⚙️ ❌ Service 异常 ======\n" +
"🔧 Method : {}\n" +
"🛑 Error : {}\n" +
"=============================",
methodName, e.getMessage(), e);
throw e;
}
long end = System.currentTimeMillis();
logger.info("\n====== ⚙️ ✅ Service 返回结果 ======\n" +
"🔧 Method : {}\n" +
"📤 Result : {}\n" +
"⏱️ 耗时 : {} ms\n" +
"===============================",
methodName, result, end - start);
return result;
}
}
七、完整流程总结
- 引入依赖 :确保
spring-boot-starter-aop
已添加 - 配置属性:通过配置文件灵活控制切面开关
- 定义切点:用表达式或注解指定拦截范围
- 实现切面 :用
@Aspect
、@Around
等实现日志逻辑 - 自定义注解:实现更细粒度的日志控制
- 应用注解:在需要记录日志的方法上加上自定义注解
八、常见问题
- 切面不生效? 检查
@Component
、@Aspect
是否加上,切点表达式是否正确,AOP依赖是否引入。 - 日志打印不全? 检查日志级别、切面逻辑是否被条件限制跳过。
AOP 的主要好处
关注点分离(Separation of Concerns)
AOP 允许你将与业务逻辑无关的"横切关注点"(如日志记录、安全控制、异常处理、事务管理)从核心业务代码中分离出去,使业务逻辑更专注、更清晰。
避免重复代码,提高可维护性
常见的重复操作如打印日志、性能统计、权限校验,如果分散在各个方法中,会导致维护困难。AOP 将这些重复逻辑集中在一个地方,修改一次即可生效全局。
不侵入业务代码
通过 AOP,你可以在不修改原方法的前提下,增强其功能(如记录参数、返回值、异常信息等),实现"开闭原则":对扩展开放,对修改封闭。
增强系统的可观测性与调试能力
配合 AOP 自动记录方法调用轨迹、执行耗时、输入输出信息,极大提升问题排查和性能分析的效率。
灵活可配置
结合注解、自定义属性、开关控制(如 AopDebugProperties
),你可以根据环境或条件动态启用/禁用某些切面逻辑,适应多种部署或调试场景。
提高开发效率
开发人员无需手动添加日志、异常处理等模板式代码,只需专注于业务逻辑,其余交由统一切面处理,显著提高开发效率和代码一致性。
总结一句话
AOP 帮你在"不碰业务代码"的前提下,实现系统级增强,让代码更干净、功能更强大、维护更轻松。
通过AOP切面,你可以优雅地实现日志记录、权限校验等横切关注点,极大提升代码的可维护性和可扩展性。希望本文能帮助你快速上手Spring Boot的AOP切面开发!