AOP
开篇:不改原有代码就能加功能?
想象你买了一份生日礼物,已经包装好了。现在你想在外面再加一层彩带、一张贺卡、一个蝴蝶结——你不需要拆开礼物重新包装,只需要在外面"装饰"就行。
Spring AOP 做的就是这件事:不修改原有业务代码,就能给它加上日志、权限校验、事务管理等通用功能。 这些功能像"装饰"一样,横切在多个业务模块之上,所以叫"面向切面编程"(Aspect-Oriented Programming)。
AOP 是 OOP 的补充而非替代。OOP 按"类和对象"纵向组织代码,AOP 按"切面"横向抽取通用逻辑。两者配合,让代码既有结构,又不冗余。
一、AOP 核心概念
AOP 里有几个术语,初学时容易搞混。我用一个"安检门"的类比来解释:
你去机场坐飞机(执行业务方法),在登机口前有一道安检门(切面)。安检门知道在哪里拦截你(切入点),在你通过前检查你的证件(前置通知),在你通过后记录你的登机信息(后置通知),如果发现违禁品就把你拦下来(异常通知)。
对应到 AOP 术语:
| 术语 | 英文 | 含义 | 类比 |
|---|---|---|---|
| 切面 Aspect | Aspect | 横切关注点的模块化,由切入点 + 通知组成 | 安检门本身(包含所有检查逻辑) |
| 连接点 JoinPoint | JoinPoint | 程序执行中的某个点(Spring 中只支持方法级别) | 每个旅客通过安检门的瞬间 |
| 切入点 Pointcut | Pointcut | 定义在哪些连接点上做拦截(充当 where 角色) | 安检门安装在哪个登机口 |
| 通知 Advice | Advice | 在切入点执行的具体逻辑(充当 what 角色) | 检查证件、记录信息等具体动作 |
| 目标对象 Target | Target | 被增强的原始对象 | 旅客本人 |
| 织入 Weaving | Weaving | 把切面应用到目标对象并创建代理的过程 | 把安检门安装到登机口 |
Spring AOP 采用运行时织入(通过动态代理),而 AspectJ 支持编译期织入和类加载期织入。Spring AOP 更简单,AspectJ 更强大。
二、JDK 动态代理 vs CGLIB
Spring AOP 的底层实现就是动态代理。根据目标对象是否实现接口,Spring 会自动选择代理方式:
两者的详细对比:
| 维度 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|
| 实现原理 | 基于 Proxy + InvocationHandler,在运行时创建实现相同接口的代理类 | 基于 ASM 字节码操作,在运行时生成目标类的子类 |
| 前提条件 | 目标对象必须实现至少一个接口 | 不需要接口,但不能代理 final 类和 final 方法 |
| 代理对象类型 | 代理类实现了目标接口 | 代理类是目标类的子类 |
| 性能 | 调用时通过反射,方法多时略慢 | 直接调用子类方法,性能略好 |
| Spring 默认 | SpringBoot 2.x 之前,有接口时默认用 JDK | SpringBoot 2.x 起默认全部用 CGLIB |
简单记:有接口用 JDK 代理,没接口用 CGLIB。 不过 SpringBoot 2.x 之后默认全部用 CGLIB 了(通过 spring.aop.proxy-target-class=true)。
代理的本质
不管哪种方式,代理的核心思想都是一样的:在调用目标方法前后,插入额外的逻辑。
调用方 → 代理对象 → [前置逻辑] → 目标方法 → [后置逻辑] → 返回结果所以,如果你的调用没有经过代理对象,AOP 就不会生效。这也是 AOP 失效的根本原因(第五节会详细讲)。
代理对象在哪里创建?
Spring 在 Bean 初始化的最后阶段创建代理对象。具体来说,是在 initializeBean() 方法中调用 applyBeanPostProcessorsAfterInitialization() 时,由 AbstractAutoProxyCreator 这个 BeanPostProcessor 完成的:
核心逻辑在 wrapIfNecessary() 方法中:先通过 getAdvicesAndAdvisorsForBean() 查找匹配当前 Bean 的切面(Advisor),如果找到了,就用 createProxy() 创建代理对象。
三、五种通知类型
Spring AOP 提供了五种通知类型,覆盖了方法执行的各个时机:
| 通知类型 | 注解 | 执行时机 | 典型用途 |
|---|---|---|---|
| 前置通知 | @Before | 目标方法执行前 | 权限校验、参数校验 |
| 后置通知 | @AfterReturning | 目标方法正常返回后 | 记录返回值、结果处理 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常后 | 异常日志、告警通知 |
| 最终通知 | @After | 目标方法执行后(无论成功/异常,类似 finally) | 资源清理 |
| 环绕通知 | @Around | 包裹目标方法执行的全过程,最灵活 | 计时统计、事务管理、缓存 |
其中 @Around 最强大,它可以完全控制目标方法的执行——包括是否执行、执行几次、修改参数和返回值:
@Aspect
@Component
public class TimingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object timing(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed(); // 执行目标方法
return result;
} finally {
long cost = System.currentTimeMillis() - start;
System.out.println(pjp.getSignature().getName() + " 耗时: " + cost + "ms");
}
}
}多个切面的执行顺序可以通过
@Order注解控制,数字越小优先级越高。
执行顺序
通知的执行顺序可以用一个"洋葱模型"来理解——外层先进后出:
Spring 内部使用递归 + 责任链模式来实现这个执行顺序:把所有匹配的拦截器组成一个链,通过递归调用逐个执行。
四、AOP 实战场景
场景一:统一日志记录
几乎所有项目都需要记录方法的调用日志。用 AOP 可以做到零侵入:
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Before("serviceLayer()")
public void logBefore(JoinPoint jp) {
System.out.println("[调用] " + jp.getSignature().toShortString()
+ ",参数: " + Arrays.toString(jp.getArgs()));
}
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void logAfter(JoinPoint jp, Object result) {
System.out.println("[返回] " + jp.getSignature().toShortString()
+ ",结果: " + result);
}
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void logException(JoinPoint jp, Throwable ex) {
System.out.println("[异常] " + jp.getSignature().toShortString()
+ ",异常: " + ex.getMessage());
}
}场景二:自定义注解 + AOP 实现权限校验
这是最优雅的 AOP 使用方式——定义一个注解作为切入点标记。
第一步,定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String value();
}第二步,编写切面:
@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(permission)")
public Object check(ProceedingJoinPoint pjp,
RequirePermission permission) throws Throwable {
String required = permission.value();
String currentUser = SecurityContext.getCurrentUser();
if (!hasPermission(currentUser, required)) {
throw new AccessDeniedException("用户 " + currentUser + " 缺少权限: " + required);
}
return pjp.proceed();
}
}第三步,在需要权限校验的方法上加注解即可:
@RequirePermission("order:create")
public Order createOrder(OrderRequest request) {
// 纯粹的业务逻辑,不包含任何权限代码
}这种模式在实际项目中非常常见——防止表单重复提交、接口限流、数据权限过滤等都可以用类似的方式实现。
场景三:声明式事务
Spring 的 @Transactional 就是 AOP 最经典的应用。它的底层是一个环绕通知(TransactionInterceptor 实现了 MethodInterceptor),在方法执行前开启事务,正常返回时提交,抛出异常时回滚。
@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, BigDecimal amount) {
accountDao.debit(from, amount); // 扣款
accountDao.credit(to, amount); // 入账
// 如果入账时抛异常,扣款也会回滚
}事务失效的常见原因,本质上都是 AOP 失效:
| 失效原因 | 说明 |
|---|---|
方法是 private 的 | 代理无法覆写私有方法 |
| 同一个类内部自调用 | this.method() 走原始对象 |
异常被 catch 吞掉 | 代理感知不到异常,无法触发回滚 |
rollbackFor 没配对 | 默认只回滚 RuntimeException 和 Error |
propagation 设置错误 | 比如 NOT_SUPPORTED 会挂起当前事务 |
| 类没被 Spring 管理 | 不是 Bean 就没有代理 |
五、AOP 什么时候会失效?
AOP 的原理是动态代理,所以只要调用没有经过代理对象,AOP 就会失效。以下是五种常见的失效场景:
1. 同一个类中的自调用(最常见的坑)
@Service
public class OrderService {
public void createOrder() {
this.validate(); // 自调用,走的是原始对象,AOP 不生效!
}
@RequirePermission("order:validate")
public void validate() {
// 这个方法上的切面不会执行
}
}解决方案:注入自身 @Autowired private OrderService self;,然后用 self.validate() 调用;或者使用 AopContext.currentProxy() 获取代理对象。
2. 私有方法
代理是通过实现接口(JDK)或继承子类(CGLIB)实现的,无论哪种方式都无法覆写 private 方法。
3. 静态方法
static 方法属于类本身,不属于对象实例。代理对象是实例级别的,自然拦截不到静态方法。
4. final 方法/类
CGLIB 通过继承目标类来生成代理子类,final 方法无法被子类覆写,final 类无法被继承。
5. 内部类方法
内部类的实例不是 Spring 管理的 Bean,调用内部类方法不会经过代理。
一句话总结:AOP 依赖代理对象,绕过代理就失效。
六、常见面试题精选
Q1:Spring AOP 和 AspectJ 的区别?
Spring AOP 是运行时通过动态代理实现的,只支持方法级别的切点,使用简单,适合业务开发。AspectJ 是编译期/类加载期织入,支持字段、构造器等更细粒度的切点,功能更强大但配置更复杂。日常 Spring 项目用 Spring AOP 就足够了。
Q2:AOP 的实现原理是什么?
Spring 在 Bean 初始化阶段,通过 AbstractAutoProxyCreator(一个 BeanPostProcessor)的后置处理方法检查 Bean 是否需要代理。如果需要(即有匹配的 Advisor),就用 JDK 或 CGLIB 创建代理对象。方法被调用时,代理对象通过递归 + 责任链模式依次执行拦截器链中的所有通知。
Q3:@Transactional 为什么会失效?
@Transactional 的底层就是 AOP。所以一切导致 AOP 失效的原因都会导致事务失效:私有方法、自调用、异常被吞、非 Spring 管理的类、rollbackFor 没配对等。
Q4:如何使用 AOP?
三步走:引入 spring-boot-starter-aop 依赖 → 定义切面类(@Aspect + @Component) → 在切面中用 @Pointcut 定义切入点、用 @Before/@Around 等定义通知。最佳实践是用自定义注解作为切入点,这样只有标注了注解的方法才会被拦截,精确且可读性好。
小结
| 概念 | 一句话总结 |
|---|---|
| AOP | 不改原有代码,给方法"穿外套"——加日志、加权限、加事务 |
| 动态代理 | AOP 的底层实现,有接口用 JDK 代理,没接口用 CGLIB |
| 五种通知 | Before / AfterReturning / AfterThrowing / After / Around(最灵活) |
| 切面执行时机 | Bean 初始化的后置处理阶段创建代理对象 |
| 失效场景 | 自调用、private、static、final——只要不走代理,AOP 就不生效 |
| 最佳实践 | 自定义注解 + @Around,优雅又灵活 |
AOP 的本质是关注点分离:让业务代码只关心业务逻辑,通用的横切关注点交给切面处理。理解了动态代理和五种通知类型,AOP 就没什么神秘的了。