Spring AOP 原理
核心本质:面向切面编程,通过代理模式,在不修改目标方法代码的前提下,对方法进行增强(如日志记录、权限校验、事务控制),实现“关注点分离”(业务逻辑与非业务逻辑分离)。
一、AOP 核心概念 🔴
5个核心概念,用“一句话+通俗解释”记,避免混淆,重点区分「连接点」和「切点」。
| 核心概念 | 官方定义 | 通俗解释 | 面试关键点 |
|---|---|---|---|
| 连接点(JoinPoint) | 程序执行过程中可被拦截的点(如方法调用、字段赋值、异常抛出) | 所有可能被增强的“位置”,Spring AOP 中仅支持「方法级连接点」 | 范围最广,是“候选增强点” |
| 切点(Pointcut) | 从所有连接点中筛选出的、需要被增强的点(通过表达式指定) | 真正要增强的“具体方法”,比如“所有 Service 类的 add 方法” | 范围比连接点小,是“选中的增强点”,常用 execution 表达式 |
| 通知(Advice) | 在切点处执行的增强逻辑(如日志、权限校验) | “增强的具体代码”,比如“在 add 方法执行前打印日志” | 分5种类型,重点记执行顺序 |
| 切面(Aspect) | 切点 + 通知的组合,封装了“什么时候增强、增强什么、怎么增强” | 一个完整的增强逻辑类,比如“日志切面”(包含切点和日志通知) | AOP 的核心载体,将通知和切点绑定 |
| 织入(Weaving) | 将切面的增强逻辑融入到目标对象的过程 | Spring 自动完成,将通知代码“织入”到目标方法的执行流程中 | Spring AOP 是“运行时织入”,基于动态代理实现 |
连接点是“所有可能的点”,切点是“选中的点”,通知是“点上的操作”,切面是“点+操作的组合”,织入是“把操作放到点上”。
二、两种代理方式:JDK 动态代理 vs CGLIB 代理(核心难点)
Spring AOP 的底层实现依赖「动态代理」,默认逻辑:目标对象有接口 → JDK 动态代理;无接口 → CGLIB 代理(可通过配置强制使用 CGLIB)。
1. 核心区别与对比 🔴
| 对比维度 | JDK 动态代理 | CGLIB 代理 |
|---|---|---|
| 底层原理 | 基于 Java 反射机制,生成目标接口的代理类(实现目标接口) | 基于 ASM 字节码技术,生成目标类的子类(继承目标类) |
| 依赖条件 | 必须有目标接口(无接口则无法使用) | 无需接口,直接代理类(可代理无接口的类) |
| 代理对象 | 代理类实现目标接口,与目标对象“平级”(不是子类) | 代理类继承目标类,是目标对象的“子类” |
| 局限性 | 无法代理无接口的类;无法代理接口中的 static/final 方法 | 无法代理 final 类(不能继承);无法代理 final 方法(不能重写) |
| 性能 | 反射效率较低(JDK 8 后优化明显,与 CGLIB 差距缩小) | 字节码生成效率高,运行时性能优于 JDK 代理(生成子类,直接调用方法) |
| Spring 默认选择 | 目标对象有接口时,优先使用 | 目标对象无接口时,自动使用;也可通过配置强制使用 |
2. 适用场景 🔴
-
JDK 动态代理:目标对象有接口(如 Service 接口+实现类),是 Spring 默认首选;
-
CGLIB 代理:目标对象无接口(如普通 POJO 类),或需要代理类的非接口方法,可手动配置开启(spring.aop.proxy-target-class=true)。
三、代理对象创建时机 🟠
核心结论:默认是“容器启动时创建”(预加载),但懒加载场景下,第一次使用时创建,与 Bean 的懒加载(@Lazy)绑定。
-
默认场景(无 @Lazy):
-
Spring 容器启动时,会初始化所有单例 Bean;
-
如果 Bean 被切面增强(需要代理),容器会在创建目标 Bean 后,立即生成代理对象,并存入容器;
-
后续获取 Bean 时,拿到的是“代理对象”,而非目标对象。
-
-
懒加载场景(@Lazy 注解):
-
容器启动时,不创建目标 Bean 和代理对象;
-
第一次调用该 Bean 的方法时,才会创建目标 Bean,生成代理对象,并返回代理对象;
-
后续调用,直接复用已创建的代理对象。
-
关键提醒:无论哪种时机,最终从容器中获取的都是代理对象,目标对象被代理对象包裹,增强逻辑在代理对象中执行。
四、五种通知类型及执行顺序 🟠
Spring AOP 支持5种通知类型,核心:「正常执行」和「异常执行」两种场景的顺序,尤其是 @Around 通知的特殊性(环绕通知,可控制目标方法执行)。
1. 五种通知类型(按常用程度排序)
| 通知类型 | 注解 | 执行时机 | 核心作用 |
|---|---|---|---|
| 环绕通知 | @Around | 目标方法执行前后,可控制目标方法是否执行、何时执行 | 最强大的通知,可实现日志、事务、权限等所有增强逻辑 |
| 前置通知 | @Before | 目标方法执行之前执行 | 做前置准备(如参数校验、日志记录开始) |
| 后置通知 | @After | 目标方法执行之后执行(无论成功/失败,都会执行) | 做清理工作(如关闭资源、日志记录结束) |
| 返回通知 | @AfterReturning | 目标方法正常执行完成后执行(异常时不执行) | 处理返回结果(如打印返回值、结果校验) |
| 异常通知 | @AfterThrowing | 目标方法抛出异常后执行(正常执行时不执行) | 处理异常(如异常日志、异常回滚) |
2. 执行顺序 🔴
场景1:目标方法正常执行
顺序:@Around(前置) → @Before → 目标方法 → @Around(后置) → @After → @AfterReturning
场景2:目标方法抛出异常
顺序:@Around(前置) → @Before → 目标方法 → @Around(异常处理) → @After → @AfterThrowing
记忆口诀
环绕先前后,前置在目标前;后置必执行,返回异常二选一(正常返回、异常抛错,二选一执行)。
关键提醒
-
@Around 是唯一能控制目标方法执行的通知(通过 ProceedingJoinPoint.proceed() 调用目标方法);
-
@After 无论成功失败都执行,优先级高于 @AfterReturning 和 @AfterThrowing;
-
多个同类型通知,可通过 @Order 注解指定执行顺序(值越小,执行越早)。
五、AOP 失效场景 🔴
核心原因:AOP 基于动态代理实现,若目标方法无法被代理对象拦截,AOP 增强就会失效,常见6种场景,重点前3种。
1. 内部调用(最常见)
场景:同一个 Bean 中,方法 A 调用方法 B,而方法 B 被 AOP 增强,此时增强逻辑不生效。
|
|
原因:this 指向的是“目标对象”,而非“代理对象”,内部调用未走代理对象的拦截逻辑,AOP 无法织入增强。
解决方案:通过 ApplicationContext 获取代理对象,或用 @Autowired 注入自身(代理对象),再调用方法。
2. Final 方法 / Static 方法(无法代理)
-
final 方法:CGLIB 无法继承 final 类、重写 final 方法,JDK 代理也无法代理 final 方法,增强失效;
-
static 方法:动态代理(JDK/CGLIB)仅能代理实例方法,static 方法属于类级别的方法,无法被代理,增强失效。
解决方案:避免对 final、static 方法做 AOP 增强,或修改方法修饰符。
3. 非 Public 方法(默认不代理)
Spring AOP 默认只对 public 方法进行代理,private、protected、default 修饰的方法,AOP 增强不生效。
原因:Spring AOP 的切点表达式(如 execution(public * com..*()))默认匹配 public 方法,非 public 方法无法被切点匹配。
解决方案:修改方法为 public,或自定义切点表达式匹配非 public 方法(不推荐,不符合规范)。
4. 其他失效场景(了解即可)
-
目标对象不是 Spring 容器管理的 Bean(如手动 new 的对象,未被 IOC 管理,无法生成代理);
-
切面未被 Spring 扫描到(如未加 @Aspect、@Component 注解);
-
切点表达式错误(未匹配到目标方法,增强逻辑无法织入)。
六、自定义切面实现方式(注解式 + 接口式)
实际开发中,注解式切面最常用(简洁、灵活),接口式切面(XML 配置)已基本淘汰,重点掌握注解式实现。
1. 注解式切面(推荐)
核心步骤:加注解(@Aspect + @Component)→ 定义切点 → 定义通知,直接上可运行代码(默写专用)。
|
|
关键提醒:@Pointcut 注解的切点表达式,是核心,常用 execution 表达式,格式:execution(访问修饰符 返回值 包名.类名.方法名(参数))。
2. 接口式切面(XML 配置,了解即可)
通过实现 Spring 提供的通知接口(如 MethodBeforeAdvice、AfterReturningAdvice),再通过 XML 配置切面、切点,现已基本不用,简化示例:
|
|
总结 🔴
-
AOP 核心概念:连接点(所有可能点)、切点(选中的点)、通知(增强逻辑)、切面(切点+通知)、织入(整合过程);
-
代理方式:有接口用 JDK 动态代理,无接口用 CGLIB 代理,核心区别是“实现接口”vs“继承类”;
-
代理创建时机:默认容器启动时创建,@Lazy 注解下第一次使用时创建;
-
通知顺序:正常执行(环绕前→前置→目标→环绕后→后置→返回),异常执行(环绕前→前置→目标→环绕异常→后置→异常);
-
失效场景:内部调用、final/static 方法、非 public 方法、非 Spring 管理的 Bean;
-
自定义切面:注解式(@Aspect + @Component + 切点 + 通知)最常用,可直接默写核心代码。