用通俗的方式拆解Spring AOP,让你彻底搞懂它。
一、Spring AOP的核心本质(用比喻讲清楚)
先看一个生活场景:
你去餐厅点一份「番茄炒蛋」,厨师的核心业务是:备料→炒鸡蛋→炒番茄→混合→装盘。
但整个过程中还有一些通用辅助操作:
- 炒菜前:洗手、开抽油烟机(前置)
- 炒菜中:随时调整火候(环绕)
- 炒菜后:刷锅、清理灶台(后置)
- 炒糊了:处理焦糊食材、道歉(异常)
- 炒成功:擦干净盘子边缘(返回后)
如果厨师每做一道菜,都要把“洗手、开抽油烟机、刷锅”这些步骤写进“番茄炒蛋/青椒肉丝/宫保鸡丁”的操作手册里,会出现两个问题:
- 重复代码太多(所有菜品都要写这些步骤);
- 维护成本高(比如要改“洗手流程”,得改所有菜品的手册)。
Spring AOP的本质:
把这些「通用辅助逻辑」(洗手、刷锅等)从「核心业务逻辑」(做菜)中抽离出来,通过“动态代理”的方式,在不修改核心业务代码的前提下,自动把辅助逻辑“织入”到核心业务的指定位置(比如炒菜前/后)。
对应到编程中:
- 核心业务:Controller/Service中的业务方法(比如
createOrder()、pay()); - 通用辅助逻辑:日志记录、事务管理、权限校验、性能监控等;
- AOP:帮你把这些通用逻辑“自动贴”到业务方法的执行前后/异常时,无需在每个业务方法里重复写。
二、Spring AOP的核心概念(场景对应)
先把AOP的核心术语和上面的“做菜场景”一一对应,抽象概念瞬间变具体:
| AOP术语 | 中文 | 做菜场景类比 | 编程场景类比 |
|---|---|---|---|
| Aspect | 切面 | 「厨房通用操作手册」(包含洗手、刷锅等) | 封装通用逻辑的类(比如LogAspect、AuthAspect) |
| Joinpoint | 连接点 | 做菜过程中所有可插入辅助操作的时机(备料前、装盘后等) | 业务方法执行过程中的所有时机(方法执行前/后/异常时) |
| Pointcut | 切入点 | 筛选要执行辅助操作的时机(只给炒蛋类菜品执行洗手流程) | 筛选要织入切面的连接点(比如只给order包下的方法加日志) |
| Advice | 通知 | 具体的辅助操作+执行时机(备料前洗手、装盘后刷锅) | 切面中的具体逻辑+执行时机(Before/After/Around等) |
| Weaving | 织入 | 把洗手/刷锅步骤加到炒番茄炒蛋的流程里 | 把切面逻辑动态融合到业务方法执行过程中 |
| Target Object | 目标对象 | 正在做菜的厨师 | 被增强的业务类(比如OrderService) |
| Proxy | 代理对象 | 带辅助流程的“增强版厨师” | Spring生成的、包含切面逻辑的代理类 |
三、代码示例(Spring Boot + AOP)
用最常见的「日志记录」场景演示,让你直观看到AOP的用法:
1. 引入依赖(pom.xml)
首先要引入AOP的核心依赖:
<!-- Spring AOP核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 核心业务类(Target Object)
模拟订单服务的核心业务:
import org.springframework.stereotype.Service;
// 目标对象:核心业务类
@Service
public class OrderService {
// 连接点:这个方法的执行时机(执行前/后/异常时)都是连接点
public String createOrder(String orderId) {
System.out.println("核心业务:创建订单,订单号=" + orderId);
// 模拟异常场景(可注释掉测试异常通知)
// if (orderId.equals("error")) {
// throw new RuntimeException("订单创建失败");
// }
return "订单创建成功:" + orderId;
}
public void payOrder(String orderId) {
System.out.println("核心业务:支付订单,订单号=" + orderId);
}
}
3. 定义切面类(Aspect)
抽离日志逻辑,指定切入点和通知:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
// 1. 标记为切面类(Aspect)
@Aspect
// 2. 交给Spring管理
@Component
public class LogAspect {
// 3. 定义切入点(Pointcut):筛选要增强的方法
// 表达式含义:匹配OrderService类下的所有public方法
@Pointcut("execution(public * com.example.demo.service.OrderService.*(..))")
public void orderPointcut() {}
// 4. 前置通知(Before):切入点方法执行前执行
@Before("orderPointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
// 获取方法名和参数
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("[前置通知] 方法" + methodName + "开始执行,参数:" + args[0]);
}
// 5. 返回通知(AfterReturning):方法正常返回后执行
@AfterReturning(value = "orderPointcut()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[返回通知] 方法" + methodName + "执行成功,返回值:" + result);
}
// 6. 异常通知(AfterThrowing):方法抛出异常时执行
@AfterThrowing(value = "orderPointcut()", throwing = "e")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception e) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[异常通知] 方法" + methodName + "执行失败,异常:" + e.getMessage());
}
// 7. 后置通知(After):方法无论成功/失败都会执行(类似finally)
@After("orderPointcut()")
public void afterAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[后置通知] 方法" + methodName + "执行结束(无论成功/失败)");
}
// 8. 环绕通知(Around):最强的通知,可控制方法的执行(前置+执行+后置)
// 注:一般不用和其他通知混用,避免逻辑混乱
// @Around("orderPointcut()")
// public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// String methodName = joinPoint.getSignature().getName();
// System.out.println("[环绕前置] 方法" + methodName + "准备执行");
// // 执行核心业务方法
// Object result = joinPoint.proceed();
// System.out.println("[环绕后置] 方法" + methodName + "执行完成,返回值:" + result);
// return result;
// }
}
4. 测试类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class AopDemoApplication {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(AopDemoApplication.class, args);
OrderService orderService = context.getBean(OrderService.class);
// 执行核心业务方法
orderService.createOrder("O20251229001");
// 测试异常场景:orderService.createOrder("error");
System.out.println("------------------------");
orderService.payOrder("O20251229001");
}
}
5. 执行结果
[前置通知] 方法createOrder开始执行,参数:O20251229001
核心业务:创建订单,订单号=O20251229001
[返回通知] 方法createOrder执行成功,返回值:订单创建成功:O20251229001
[后置通知] 方法createOrder执行结束(无论成功/失败)
------------------------
[前置通知] 方法payOrder开始执行,参数:O20251229001
核心业务:支付订单,订单号=O20251229001
[返回通知] 方法payOrder执行成功,返回值:null
[后置通知] 方法payOrder执行结束(无论成功/失败)
四、新手容易误解的关键点
-
混淆连接点(Joinpoint)和切入点(Pointcut)
- 错误认知:以为两者是同一个东西;
- 正确理解:连接点是“所有可能的时机”(比如OrderService的所有方法的执行前/后),切入点是“筛选后的时机”(比如只选createOrder方法的执行前)。
- 类比:连接点是餐厅所有菜品,切入点是“只选川菜”。
-
认为AOP只能做日志
- 错误认知:AOP=日志记录;
- 正确理解:日志只是AOP的常见场景,它还能做事务管理(Spring声明式事务底层就是AOP)、权限校验、性能监控、缓存控制等。
-
滥用环绕通知(Around)
- 错误认知:环绕通知功能强,所有场景都用它;
- 正确理解:环绕通知需要手动调用
joinPoint.proceed()执行核心方法,逻辑复杂,新手容易漏写导致核心方法不执行。只有需要“控制核心方法是否执行/修改参数/修改返回值”时才用,普通日志/校验用Before/After即可。
-
以为AOP修改了原业务代码
- 错误认知:AOP是改了OrderService的源码;
- 正确理解:Spring AOP基于动态代理实现,运行时生成代理对象(Proxy),原业务类(Target Object)的代码完全没改,只是执行时走的是代理对象的逻辑。
-
切入点表达式写错导致切面不生效
- 常见错误:包名写错、方法修饰符漏写(比如把public写成private)、参数匹配错误(
(..)表示任意参数,新手会写成()导致只匹配无参方法); - 避坑技巧:写表达式时先简化(比如先匹配所有方法
execution(* *(..))),确认生效后再缩小范围。
- 常见错误:包名写错、方法修饰符漏写(比如把public写成private)、参数匹配错误(
总结
- 核心本质:Spring AOP是“面向切面编程”,把通用辅助逻辑(日志、事务等)从核心业务中抽离,动态织入到指定位置,解决代码重复、降低耦合。
- 核心逻辑:通过「切面(Aspect)」封装通用逻辑,「切入点(Pointcut)」筛选目标方法,「通知(Advice)」指定执行时机和逻辑,最终通过「织入」增强核心业务。
- 新手避坑:区分连接点和切入点、避免滥用环绕通知、注意切入点表达式的正确性,且AOP不会修改原业务代码。