Spring AOP 核心精讲
Spring AOP 核心精讲
本文将全面拆解 Spring AOP 的核心概念、底层动态代理机制、五大术语、实战代码以及真实业务中的失效坑点。内容涵盖面试高频追问与开发落地指南,做到「面试能背、开发能用、场景全覆盖」。
一、 核心思想:OOP vs AOP(通俗理解与面试开场)
1. 面试满分开场白
AOP(面向切面编程)是一种横向抽取公共重复逻辑的编程思想。它可以在不修改原有业务代码的前提下,对目标方法动态增强日志、事务、权限校验、接口监控等通用功能。其底层核心实现依赖 Spring 的动态代理机制(JDK 动态代理与 CGLIB 代理)。
2. 通俗理解:OOP 与 AOP 的本质区别
OOP(面向对象编程):纵向拆分
- 通俗比喻:造汽车。OOP 负责把汽车拆成发动机、底盘、轮胎等模块,分别制造,解决的是业务代码的分层与模块复用问题。
- 局限性:如果所有汽车出厂前都需要“喷漆”和“安检”,OOP 只能在每个车间(业务方法)里都写一遍喷漆和安检代码,导致代码冗余。
AOP(面向切面编程):横向抽取
- 通俗比喻:汽车出厂流水线上的“统一安检站”。AOP 不关心汽车怎么造,它只在汽车出厂的必经之路上横插一刀,统一进行安检和喷漆。解决的是分散在所有业务方法中的通用重复代码问题。
二、 AOP 五大核心术语(通俗释义 + 切点实操案例)
统一业务场景:使用 AOP 给所有 Service 层方法统一添加日志监控与权限校验。
1. 连接点(JoinPoint)
- 定义:程序中可以被 AOP 拦截、可以被增强的所有候选位置。在 Spring AOP 中,特指所有
public业务方法。 - 通俗比喻:工厂里所有可以安装安检门的地方(大门、侧门、后门)。
- 举例:
UserService的addUser()、updateUser()、deleteUser()全部都是连接点。
2. 切点(Pointcut)
- 定义:通过切点表达式,从所有连接点中筛选出来、真正要拦截增强的方法集合。
- 通俗比喻:经过评估,决定只在大门和侧门安装安检门,这就是切点。
- 核心区别:连接点是“所有候选”,切点是“最终选中生效的”。
切点表达式实操案例(开发最常用):
// 1. execution:按方法签名匹配(最基础)
// 拦截 com.xxx.service 包及其子包下,所有类的所有方法
@Pointcut("execution(* com.xxx.service..*.*(..))")
public void servicePointCut() {}
// 2. @annotation:按自定义注解匹配(企业开发最常用、最灵活)
// 拦截所有标注了 @RequireLogin 注解的方法
@Pointcut("@annotation(com.xxx.annotation.RequireLogin)")
public void loginPointCut() {}
// 3. within:按类匹配
// 拦截 com.xxx.controller 包下所有类的所有方法
@Pointcut("within(com.xxx.controller..*)")
public void controllerPointCut() {}
// 4. 组合切点:使用 &&、||、! 进行逻辑运算
// 拦截 service 包下,且带有 @RequireLogin 注解的方法
@Pointcut("servicePointCut() && @annotation(com.xxx.annotation.RequireLogin)")
public void combinedPointCut() {}
3. 通知(Advice)
- 定义:拦截到切点方法后,需要执行的增强逻辑代码(AOP 真正要做的事)。
- 通俗比喻:安检员的具体动作(查身份证、过安检机、登记信息)。
| 通知注解 | 执行时机 | 核心用途 |
|---|---|---|
@Before | 目标方法执行前 | 权限校验、参数打印、前置参数校验 |
@AfterReturning | 目标方法正常执行成功返回后 | 正常日志记录、返回值数据脱敏/后置处理 |
@AfterThrowing | 目标方法抛出异常时 | 异常日志记录、告警通知、事务回滚辅助 |
@After | 目标方法最终执行完毕(无论成功/异常) | 资源释放、ThreadLocal 清理、统一收尾操作 |
@Around | 包裹目标方法,全权控制执行流程 | 性能计时、事务控制、分布式锁、全流程拦截(最强通知) |
4. 切面(Aspect)
- 定义:切点 + 通知 的结合体,是存放所有 AOP 增强逻辑的工具类。
- 通俗比喻:安检站的整体方案(包含在哪里安检、安检员做什么动作)。
- 本质:被
@Aspect+@Component标记的普通 Java 类。
5. 目标对象(Target)与 代理对象(Proxy)
- 目标对象:被 AOP 增强的原始业务对象(如原生的
UserServiceImpl)。 - 代理对象:Spring 自动生成的代理实例。客户端所有调用都会先走代理对象,由代理对象触发切面逻辑,再调用目标业务方法。
三、 底层核心:两种动态代理机制与选择规则
AOP 的所有功能完全依赖动态代理实现。Spring 会根据目标类的特征自动选择代理方式。
1. 代理机制对比
| 维度 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 核心规则 | 基于接口代理 | 基于继承子类代理 |
| 生效条件 | 目标类必须实现接口 | 无需实现接口,通过生成子类重写方法增强 |
| 底层原理 | 利用反射机制生成一个实现代理接口的匿名类 | 利用 ASM 字节码技术生成一个被代理类的子类 |
| 失效场景 | 目标类无接口时无法使用 | 目标类或方法被 final 修饰时无法重写,导致失效 |
| 性能表现 | 生成代理类速度快,运行时反射调用稍慢 | 生成代理类速度慢,运行时直接调用方法,性能更高 |
2. Spring 代理选择规则(面试必背)
- 目标类有接口:默认优先使用 JDK 动态代理。
- 目标类无接口:自动使用 CGLIB 代理。
- Spring Boot 2.0 及以上版本:默认强制使用 CGLIB 代理(通过
spring.aop.proxy-target-class=true配置),以避免接口新增方法导致代理失效的问题。
3. 动态代理执行流程图
四、 完整可落地 AOP 实战代码
1. 自定义注解(企业级开发最常用方式)
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {
String module() default "";
String action() default "";
}
2. 核心切面类(包含 5 种通知与自定义注解切点)
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect
@Component
public class OperateLogAspect {
// 切点:拦截所有标注了 @OperateLog 的方法
@Pointcut("@annotation(com.xxx.annotation.OperateLog)")
public void logPointCut() {}
// 1. 前置通知
@Before("logPointCut()")
public void before(JoinPoint joinPoint) {
System.out.println("[Before] 方法开始执行:" + joinPoint.getSignature().getName()
+ ",参数:" + Arrays.toString(joinPoint.getArgs()));
}
// 2. 正常返回通知
@AfterReturning(value = "logPointCut()", returning = "result")
public void afterReturn(Object result) {
System.out.println("[AfterReturning] 方法执行成功,返回结果:" + result);
}
// 3. 异常通知
@AfterThrowing(value = "logPointCut()", throwing = "e")
public void afterThrow(Exception e) {
System.out.println("[AfterThrowing] 方法执行异常,异常信息:" + e.getMessage());
}
// 4. 后置通知(最终通知)
@After("logPointCut()")
public void after() {
System.out.println("[After] 方法执行结束,清理 ThreadLocal 等资源");
}
// 5. 环绕通知(最强通知,手动控制目标方法执行)
@Around("logPointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("[Around-Pre] 开始统计方法耗时");
long start = System.currentTimeMillis();
try {
// 核心:手动执行目标业务方法
Object result = pjp.proceed();
return result;
} finally {
long time = System.currentTimeMillis() - start;
System.out.println("[Around-Post] 方法执行耗时:" + time + "ms");
}
}
}
3. 业务 Service 测试
@Service
public class UserService {
@OperateLog(module = "用户管理", action = "新增用户")
public String addUser(String username) {
System.out.println(">>> 执行核心业务:新增用户 " + username);
return "新增成功:" + username;
}
}
五、 核心业务场景:@Transactional 事务底层剖析-详细可见@EnableTanstional源码详解
@Transactional 是 Spring AOP 最经典、最核心的应用。面试中常问:“为什么加了 @Transactional 就能控制事务?”
1. 事务 AOP 底层执行流程
Spring 的事务管理完全基于 AOP 的环绕通知(@Around) 实现。底层核心类是 TransactionInterceptor。
2. @Transactional 常见踩坑点(面试必问)
- 非 public 方法失效:Spring AOP 默认只拦截 public 方法,protected/private/default 方法上加事务无效。
- 同类内部调用失效:类内部 A 方法调用 B 方法(B 有事务),因为绕过了代理对象,直接调用了原生对象,事务失效。
- 异常被 try-catch 吞掉:业务代码中 catch 了异常且没有重新抛出,AOP 切面感知不到异常,导致事务无法回滚。
- 异常类型不匹配:默认只回滚
RuntimeException和Error。如果抛出的是受检异常(如Exception、IOException),必须显式配置@Transactional(rollbackFor = Exception.class)。 - 数据库引擎不支持:如 MySQL 使用了 MyISAM 引擎(不支持事务),必须改为 InnoDB。
六、 AOP 完整执行顺序(高级面试考点)
注意:Spring 版本的升级导致了执行顺序的变化,这是区分初级和中高级候选人的关键点。
1. Spring 4.x 版本(旧版顺序)
环绕前置 -> 前置 -> 目标方法 -> 环绕后置 -> 后置 -> 返回/异常
2. Spring 5.x / Spring Boot 2.x 及以上版本(现行标准顺序)
Spring 官方在 5.x 版本中修复了通知执行的顺序,使其更符合直觉(环绕通知真正“环绕”了所有其他通知)。
标准执行链路:
@Around前置部分(pjp.proceed()之前的代码)@Before前置通知- 目标业务方法执行
@Around后置部分(pjp.proceed()之后的代码)@After最终后置通知@AfterReturning(正常) 或@AfterThrowing(异常)
七、 AOP 失效真实业务坑点与解决方案
坑点 1:同类内部方法调用(最高频)
- 现象:同一个类中,方法 A(无事务/无切面)调用方法 B(有事务/有切面),方法 B 的 AOP 增强失效。
- 原因:AOP 基于代理对象拦截。内部调用时,使用的是
this关键字(即目标对象本身),绕过了代理对象。 - 解决方案:
- 方案一(推荐):将方法 B 抽离到另一个 Service 类中,通过依赖注入调用。
- 方案二:在当前类中注入自己(
@Autowired private UserService self;),通过self.methodB()调用。 - 方案三:使用
AopContext.currentProxy()获取当前代理对象:((UserService) AopContext.currentProxy()).methodB()(需在启动类添加@EnableAspectJAutoProxy(exposeProxy = true))。
坑点 2:方法被 final 或 static 修饰
- 现象:切面配置正确,但目标方法未被拦截。
- 原因:CGLIB 基于继承生成子类,无法重写
final方法;JDK 代理基于接口,无法代理static方法。 - 解决方案:移除目标方法上的
final或static修饰符,改为普通的public实例方法。
坑点 3:切面类未被 Spring 管理
- 现象:切面类写了,但毫无反应。
- 原因:只加了
@Aspect,忘记加@Component,导致切面类本身没有被注册为 Spring Bean。 - 解决方案:确保切面类上同时存在
@Aspect和@Component(或@Configuration)。
坑点 4:多线程环境导致事务/AOP 失效
- 现象:在
@Transactional方法内部开启新线程执行 SQL,新线程的 SQL 不在事务控制范围内。 - 原因:数据库连接(Connection)是绑定在当前线程的
ThreadLocal中的。新线程无法获取原线程的事务连接。 - 解决方案:避免在事务方法中开启新线程执行数据库操作;如果必须,需使用编程式事务或传递连接对象(极不推荐)。
八、 面试高频问答(满分模板)
问 1:切点(Pointcut)和连接点(JoinPoint)的区别是什么?
答:连接点是程序中所有可以被拦截的候选方法(在 Spring 中指所有 public 方法);切点是通过表达式从连接点中筛选出来的、真正需要被增强的方法集合。连接点是“范围”,切点是“结果”。
问 2:Spring AOP 和 AspectJ 有什么区别?
答:Spring AOP 是运行时织入,基于动态代理实现,不需要额外的编译过程,但只能代理 Spring 管理的 Bean,且只能拦截方法级别;AspectJ 是编译时/类加载时织入,基于字节码修改,性能更高,功能更强大(支持字段、构造器拦截),但需要专门的编译器(ajc)。日常企业开发 Spring AOP 已完全足够。
问 3:为什么 Spring Boot 2.0 之后默认使用 CGLIB 代理?
答:JDK 动态代理要求目标类必须实现接口,且如果接口新增方法,代理类可能需要同步修改。CGLIB 基于继承,不强制要求接口,适用性更广。为了减少因代理方式切换带来的潜在 Bug,Spring Boot 2.0 开始默认强制使用 CGLIB。
问 4:@Transactional 为什么是 AOP 的典型应用?
答:Spring 事务底层完全基于 AOP 环绕通知实现。它通过动态代理拦截业务方法,在方法执行前获取连接并开启事务,正常结束提交事务,抛出异常回滚事务。全程不修改业务代码,完美契合 AOP 横向抽离公共逻辑的核心思想。
九、 极简终极记忆口诀
连接点是候选,切点挑一批;
通知加逻辑,切面装一起;
代理来执行,原类是目标;
事务日志监控,全靠 AOP 提。
内部调用绕代理,final 方法难重写;
Boot 默认 CGLIB,执行顺序五变六。
更多推荐



所有评论(0)