面向切面编程(AOP,Aspect-Oriented Programming)是 Spring 框架的核心特性之一,它通过"横切"思想,将日志、权限校验、事务管理等通用功能与业务逻辑解耦,大幅提升代码复用性和可维护性。本文将从「实例演示→实现方式→核心细节→避坑注意事项」,一步步带你掌握 AOP,内容适合新手入门,也可作为实战参考,直接用于项目开发。
一、先搞懂:AOP 核心概念(新手必看)
在写代码前,先明确 AOP 的核心术语,避免后续 confusion,用通俗的话解释,不堆砌专业名词:
-
切面(Aspect):就是我们要实现的通用功能(比如日志、权限),是一个独立的类,包含通知和切入点。
-
通知(Advice):切面的具体逻辑(比如日志要打印什么内容),分 5 种类型(后文实例会逐个演示)。
-
切入点(Pointcut):指定切面作用在哪些方法上(比如所有 controller 方法、带特定注解的方法)。
-
连接点(JoinPoint):所有可能被切面拦截的方法(比如项目中所有的接口方法),切入点是连接点的子集。
-
目标对象(Target):被切面拦截的对象(比如我们的业务ServiceImpl)。
一句话总结:用切面(Aspect)定义通用功能(通知),并指定作用在哪些方法(切入点)上,实现与业务逻辑的解耦。
二、实战实例:用 AOP 实现「接口日志记录」
我们以最常用的「接口请求日志记录」为实例,实现以下功能:
-
记录请求的 URL、请求方法(GET/POST)、请求参数、请求者 IP;
-
记录接口执行耗时;
-
记录接口返回结果(成功/失败);
-
接口抛出异常时,记录异常信息。
技术栈:Spring Boot 2.7.x + Spring AOP(Spring 自带,无需额外引入依赖)
步骤 1:创建 Spring Boot 项目,引入依赖
创建 Spring Boot 项目后,无需额外引入 AOP 依赖(Spring Boot 已自动集成 spring-boot-starter-aop),pom.xml 核心依赖如下(可直接复制):
xml
<!-- Spring Boot 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AOP 依赖(Spring Boot 自动集成,可省略,但建议显式引入,清晰) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 日志依赖(方便格式化输出,可选) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
步骤 2:编写 AOP 切面类(核心代码)
创建切面类 LogAspect,通过注解定义切面、通知和切入点,代码中包含详细注释,新手可直接复制使用:
java
package com.example.aop.demo.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
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.util.Arrays;
/**
* 接口日志切面类
* 注解说明:
* @Aspect:标识此类是一个切面类
* @Component:将切面类交给 Spring 管理(必须加,否则切面不生效)
*/
@Slf4j
@Aspect
@Component
public class LogAspect {
/**
* 切入点:指定切面作用的范围
* 表达式说明:
* execution(* com.example.aop.demo.controller..*(..))
* 1. *:返回值任意
* 2. com.example.aop.demo.controller:包名(可替换为你的 controller 包路径)
* 3. ..:当前包及子包下的所有类
* 4. *(..):所有方法(任意参数、任意方法名)
*/
@Pointcut("execution(* com.example.aop.demo.controller..*(..))")
public void logPointCut() {
// 切入点方法,无需实现任何逻辑,仅用于定义切入点表达式
}
/**
* 1. 前置通知(@Before):在目标方法执行前执行
* 作用:记录请求的基础信息(URL、IP、请求方法、参数)
*/
@Before("logPointCut()")
public void doBefore(JoinPoint joinPoint) {
// 获取请求上下文
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 打印请求信息
log.info("=== 接口请求开始 ===");
log.info("请求URL:{}", request.getRequestURI());
log.info("请求方法:{}", request.getMethod());
log.info("请求IP:{}", request.getRemoteAddr());
log.info("目标方法:{}", joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
log.info("请求参数:{}", Arrays.toString(joinPoint.getArgs()));
}
/**
* 2. 环绕通知(@Around):在目标方法执行前后都执行,可控制目标方法的执行
* 作用:计算接口执行耗时(最常用场景)
* 注意:环绕通知必须有返回值,且参数是 ProceedingJoinPoint(可执行目标方法)
*/
@Around("logPointCut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 记录方法开始执行时间
long startTime = System.currentTimeMillis();
// 执行目标方法(放行,让业务方法执行)
Object result = proceedingJoinPoint.proceed();
// 计算执行耗时
long endTime = System.currentTimeMillis();
log.info("接口执行耗时:{} ms", endTime - startTime);
// 返回目标方法的执行结果(必须返回,否则接口会返回 null)
return result;
}
/**
* 3. 后置通知(@After):无论目标方法是否抛出异常,都会执行
* 作用:标记请求结束,做一些收尾工作
*/
@After("logPointCut()")
public void doAfter() {
log.info("=== 接口请求结束 ===\n");
}
/**
* 4. 后置返回通知(@AfterReturning):目标方法正常执行完成后执行
* 作用:记录接口返回结果
* returning = "result":指定返回值的参数名,必须和方法参数一致
*/
@AfterReturning(pointcut = "logPointCut()", returning = "result")
public void doAfterReturning(Object result) {
log.info("接口返回结果:{}", result);
}
/**
* 5. 后置异常通知(@AfterThrowing):目标方法抛出异常时执行
* 作用:记录异常信息,方便排查问题
* throwing = "e":指定异常参数名,必须和方法参数一致
*/
@AfterThrowing(pointcut = "logPointCut()", throwing = "e")
public void doAfterThrowing(Throwable e) {
log.error("接口执行异常:", e); // 打印异常堆栈信息
}
}}
步骤 3:编写测试接口,验证 AOP 效果
创建一个简单的 Controller,编写两个接口(正常接口 + 异常接口),测试 AOP 是否生效:
java
package com.example.aop.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
// 正常接口:模拟查询用户
@GetMapping("/user/get")
public String getUser(@RequestParam String username) {
return "查询成功,用户:" + username;
}
// 异常接口:模拟抛出异常
@PostMapping("/user/error")
public void testError() {
int i = 1 / 0; // 除数为0,抛出异常
}
}
步骤 4:启动项目,测试效果
- 启动 Spring Boot 项目,访问正常接口:
http://localhost:8080/user/get?username=zhangsan
控制台输出(AOP 日志生效):
text
=== 接口请求开始 ===
请求URL:/user/get
请求方法:GET
请求IP:0:0:0:0:0:0:0:1
目标方法:com.example.aop.demo.controller.TestController.getUser
请求参数:[zhangsan]
接口返回结果:查询成功,用户:zhangsan
接口执行耗时:12 ms
=== 接口请求结束 ===
- 访问异常接口:
http://localhost:8080/user/error
控制台输出(异常日志生效):
text
=== 接口请求开始 ===
请求URL:/user/error
请求方法:POST
请求IP:0:0:0:0:0:0:0:1
目标方法:com.example.aop.demo.controller.TestController.testError
请求参数:[]
接口执行异常:
java.lang.ArithmeticException: / by zero
at com.example.aop.demo.controller.TestController.testError(TestController.java:18)
...(省略异常堆栈)
=== 接口请求结束 ===
至此,AOP 实例演示完成,完美实现了接口日志的统一记录,且没有侵入任何业务代码(TestController 中没有任何日志相关代码)。
三、AOP 三种实现方式(从简单到复杂)
上面实例用的是「注解式实现」(最常用),除此之外,Spring AOP 还有另外两种实现方式,适合不同场景,逐一说明:
方式 1:注解式实现(@Aspect + 注解通知)
就是上面实例用的方式,也是目前 Spring Boot 项目中最推荐的方式,优点:
-
代码简洁,无需配置文件,上手快;
-
灵活度高,切入点表达式可精准控制作用范围;
-
支持所有 5 种通知类型。
核心要点:必须加 @Aspect 和 @Component 注解,切入点用 @Pointcut 定义,通知用 @Before、@Around 等注解。
方式 2:XML 配置式实现(传统方式)
在 Spring 早期(没有注解),主要用 XML 配置实现 AOP,现在很少用,但了解即可,适合老项目维护。
步骤:1. 编写切面类(无需加任何 AOP 注解);2. 在 spring.xml 中配置切面、切入点、通知。
示例(简化版):
xml
<!-- 1. 配置切面类 -->
<bean id="logAspect" class="com.example.aop.demo.aspect.LogAspect"/>
<!-- 2. 配置 AOP -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut id="logPointCut" expression="execution(* com.example.aop.demo.controller..*(..))"/>
<!-- 配置切面,关联切入点和通知 -->
<aop:aspect ref="logAspect">
<aop:before pointcut-ref="logPointCut" method="doBefore"/>
<aop:around pointcut-ref="logPointCut" method="doAround"/>
<aop:after pointcut-ref="logPointCut" method="doAfter"/>
</aop:aspect>
</aop:config>
缺点:XML 配置繁琐,维护成本高,不推荐新项目使用。
方式 3:实现接口式实现(最原始方式)
通过实现 Spring 提供的 AOP 接口(如 MethodBeforeAdvice、AfterReturningAdvice)来实现通知,仅支持部分通知类型(无环绕通知),灵活性差,几乎不用。
示例(简化版):
java
// 实现前置通知接口
@Component
public class LogBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
// 前置通知逻辑(和 @Before 一样)
log.info("前置通知:方法执行前执行");
}
}
// XML 配置切入点(省略,和方式2类似)
总结:优先使用 注解式实现,XML 方式仅用于老项目维护,接口式实现可忽略。
四、AOP 核心注意事项(避坑关键,重中之重)
很多新手用 AOP 时会遇到"切面不生效""通知执行顺序错乱"等问题,以下注意事项必须牢记,覆盖 90% 的坑:
1. 切面类必须交给 Spring 管理(必坑)
切面类上必须加 @Component(或 @Service、@Controller),否则 Spring 无法扫描到切面,AOP 完全不生效。
错误示例:只加 @Aspect,不加 @Component → 切面无效。
2. 切入点表达式必须正确(必坑)
切入点表达式写错,切面会作用到错误的方法,甚至完全不生效,常见错误:
-
包路径写错(比如 controller 包路径少写一层);
-
表达式语法错误(比如少写
\.\.、\*位置错误); -
目标方法是 private 修饰 → AOP 无法拦截(AOP 只能拦截 public 方法)。
推荐:写完切入点后,先测试一个简单接口,验证切面是否生效。
3. 环绕通知的注意事项(高频坑)
-
必须调用
proceedingJoinPoint\.proceed\(\),否则目标方法不会执行(相当于拦截了方法,不放行); -
必须返回
proceed\(\)的结果,否则接口会返回 null(即使目标方法有返回值); -
环绕通知可以修改目标方法的参数和返回值(灵活,但需谨慎)。
4. 通知执行顺序(重点)
当一个方法被多个切面拦截,或一个切面有多个通知时,执行顺序如下(牢记):
正常情况:前置通知(@Before)→ 环绕通知(proceed() 前)→ 目标方法 → 环绕通知(proceed() 后)→ 后置返回通知(@AfterReturning)→ 后置通知(@After)
异常情况:前置通知 → 环绕通知(proceed() 前)→ 目标方法(抛异常)→ 后置异常通知(@AfterThrowing)→ 后置通知(@After)
补充:多个切面的执行顺序,可通过 @Order 注解控制(值越小,执行越早)。
5. AOP 无法拦截的情况(高频坑)
-
private、final、static 修饰的方法(AOP 基于动态代理,无法代理这些方法);
-
方法内部调用(比如同一个类中,方法 A 调用方法 B,方法 B 被切面拦截 → 不生效);
-
目标对象没有交给 Spring 管理(比如 new 出来的对象,不是 Spring 容器中的 bean)。
解决方案:内部方法调用时,通过 Spring 容器获取自身 bean,再调用方法(避免直接 this.方法名())。
6. 性能注意事项
-
切面逻辑尽量简单(比如日志记录、参数校验),避免复杂计算,影响接口性能;
-
切入点范围尽量精准(比如只作用于 controller 层,不要作用于所有层),减少拦截次数。
7. 注解式切入点(补充,更灵活)
除了 execution 表达式,还可以用自定义注解作为切入点,更灵活(比如只给需要日志的接口加注解):
java
// 1. 定义自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAnnotation {
// 可添加属性,比如日志描述
String value() default "";
}
// 2. 切入点改为注解
@Pointcut("@annotation(com.example.aop.demo.annotation.LogAnnotation)")
public void logPointCut() {}
// 3. 在接口上添加注解
@LogAnnotation("查询用户接口")
@GetMapping("/user/get")
public String getUser(@RequestParam String username) { ... }
五、AOP 常见应用场景(拓展,提升博客价值)
除了日志记录,AOP 还有很多实用场景,帮你拓展思路:
-
权限校验:拦截接口,判断用户是否登录、是否有操作权限;
-
事务管理:Spring 声明式事务(@Transactional)底层就是 AOP;
-
参数校验:统一校验接口请求参数(比如非空、格式校验);
-
接口限流:限制接口的请求频率(比如每秒最多10次请求);
-
异常统一处理:全局异常捕获,返回统一的错误格式。
六、总结
AOP 的核心是「解耦」,将通用功能抽离成切面,不侵入业务代码,提升代码复用性和可维护性。本文通过「接口日志」实例,讲解了注解式实现(最常用)、XML 实现、接口式实现三种方式,同时总结了新手常踩的坑和注意事项。
对于 CSDN 博客读者来说,本文代码可直接复制到项目中使用,同时理解核心原理,后续可根据实际场景(如权限、限流)灵活修改切面逻辑。
最后,留一个小思考:如何用 AOP 实现接口限流?欢迎在评论区交流~