第75篇:Spring AOP原理(2026精版)
📌系列导航:《Java 100 天进阶之路》完整目录 |
⬅️ 上一篇:第74篇:Bean生命周期 |
➡️ 下一篇:第76篇:Spring事务(待发布)
🗺️ 本文阅读地图(3 分钟速览)
第74篇搞定了Bean生命周期,本篇深入Spring AOP原理。AOP是Spring框架的另一大核心,
@Transactional、@Async、权限校验、日志记录等功能的底层都是AOP。理解了AOP,就理解了Spring的"增强"能力从何而来:
| 模块 | 核心问题 | 一句话回答 |
|---|---|---|
| AOP是什么 | 面向切面编程解决什么问题? | 把日志、事务等横切关注点从业务代码中抽离出来,集中管理 |
| 核心概念 | 切面、切点、通知、连接点是什么? | 切面是功能模块,切点定义"在哪里"执行,通知定义"做什么" |
| 代理机制 | AOP底层怎么实现的? | 动态代理——JDK动态代理(基于接口)或CGLIB(基于继承) |
| JDK vs CGLIB | 两种代理怎么选? | 目标类有接口用JDK,无接口用CGLIB;Spring Boot 2.x+默认CGLIB |
| 执行顺序 | 多个通知的执行顺序? | @Around(前) →@Before→ 目标方法 →@Around(后) →@After→@AfterReturning/@AfterThrowing |
| AOP失效场景 | 为什么@Transactional有时不生效? | 自调用、非public方法、异常被吞掉等 |
| 面试最爱问 | 高频考点有哪些? | 见文末 面试小节 |
一、核心知识点
1. AOP是什么?
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,旨在将横切关注点(cross-cutting concerns)与核心业务逻辑分离。横切关注点是指那些跨越多个业务模块的通用功能——日志记录、事务管理、权限控制、性能监控等。AOP通过代理模式实现了对这些功能的集中管理,使核心业务逻辑与通用功能解耦。
💡OOP vs AOP:OOP按纵向维度(继承、封装、多态)组织代码,适合描述"是什么";AOP按横向维度(切面)组织代码,适合描述"在什么时机做什么事"。两者互补,不是替代关系。
2. AOP解决了什么问题?
| 问题 | 传统方式 | AOP方式 |
|---|---|---|
| 代码重复 | 每个业务方法都要写日志、事务代码 | 切面集中定义,一处修改处处生效 |
| 耦合度高 | 业务代码混杂着非业务逻辑 | 业务类只关注核心逻辑,干净纯粹 |
| 难以维护 | 改一个日志格式要改几百个文件 | 只改切面类即可 |
| 难以扩展 | 新增功能要改所有相关类 | 新增切面类,无侵入式扩展 |
二、通俗讲解(1分钟开心学)
把AOP想象成"餐厅的中央厨房"
- 核心业务(OOP):餐厅的"菜谱"——宫保鸡丁怎么做、水煮鱼怎么做,每道菜有自己独特的做法(纵向)。
- 横切关注点(AOP):餐厅的"后勤管理"——所有菜都要洗菜(前置处理)、装盘(后置处理)、记录每道菜耗时(环绕处理)。这些事跟菜本身的做法无关,但每道菜都要做。
- 传统方式:每个厨师做菜时都要自己洗菜、装盘、记时间——代码重复,改起来麻烦。
- AOP方式:专门配一个后勤团队(切面)统一负责洗菜、装盘、记时间。厨师只管炒菜。哪天要换装盘方式,只改后勤团队,所有菜自动生效。
AOP的核心比喻:
- 切面(Aspect)= 后勤团队(日志团队、事务团队、权限团队)
- 切点(Pointcut)= 规则"所有热菜都要洗菜"(匹配哪些方法)
- 通知(Advice)= 具体动作"洗菜"(在切点执行的代码)
- 连接点(Join Point)= 每一次"洗菜"的具体时机(方法执行的某个时刻)
- 织入(Weaving)= 把后勤团队"嵌入"到厨师做菜流程中的过程
三、AOP核心概念详解
3.1 六大核心概念
Spring AOP建立在几个关键抽象之上:
| 概念 | 英文 | 通俗理解 | 示例 |
|---|---|---|---|
| 连接点 | Join Point | 程序执行过程中可以插入切面逻辑的位置(如方法调用、异常抛出) | 任何Service方法调用 |
| 切点 | Pointcut | 通过表达式匹配一组连接点,决定"在哪里"执行 | execution(* com.example.service.*.*(..)) |
| 通知 | Advice | 在特定连接点执行的具体动作,决定"做什么" | @Before记录日志、@Around开启事务 |
| 切面 | Aspect | 封装横切关注点的模块,包含通知和切点 | @Aspect标注的LogAspect类 |
| 目标对象 | Target Object | 被代理的原始业务对象 | UserServiceImpl实例 |
| 织入 | Weaving | 将切面代码与目标对象关联的过程 | Spring在运行时通过动态代理完成 |
💡Spring AOP的限制:Spring AOP仅支持方法级别的连接点——只能在方法调用时插入切面逻辑,不支持字段访问、构造器调用等更细粒度的连接点。如果需要更强大的AOP能力,需要配合AspectJ使用。
3.2 五种通知类型
Spring AOP提供了五种通知类型,在不同的时机插入横切逻辑:
| 注解 | 类型 | 执行时机 | 典型场景 |
|---|---|---|---|
@Before | 前置通知 | 目标方法执行前 | 参数校验、权限检查 |
@After | 后置通知 | 目标方法执行后(无论是否异常,类似finally) | 资源清理、释放连接 |
@AfterReturning | 返回后通知 | 目标方法正常返回后 | 记录返回值、缓存更新 |
@AfterThrowing | 异常通知 | 目标方法抛出异常后 | 异常监控、告警 |
@Around | 环绕通知 | 包裹目标方法,控制其执行流程 | 事务管理、性能监控、日志记录 |
💡
@Around最强大:它可以完全控制目标方法的执行——在方法执行前后插入逻辑,甚至可以决定是否执行目标方法。@Transactional的底层实现就是@Around通知。
四、AOP底层原理:动态代理
4.1 代理模式核心思想
Spring AOP的实现本质上依赖于代理模式。代理模式通过引入代理对象作为目标对象的中间层,实现了对目标对象访问的控制与增强。Spring AOP在运行时通过动态代理将切面代码织入目标对象。
💡核心理解:AOP的底层就是动态代理。容器中放的不是原始对象,而是代理对象。你调用
userService.saveUser()时,实际调用的是代理对象的方法——代理对象先执行切面逻辑(事务开启、日志记录),再调用原始对象的方法。
4.2 JDK动态代理 vs CGLIB
Spring AOP使用两种方式创建动态代理:
| 对比维度 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 实现方式 | 基于接口生成代理类 | 通过继承目标类生成子类 |
| 使用条件 | 目标类实现了至少一个接口 | 目标类没有实现接口(或强制指定) |
| 核心类 | java.lang.reflect.Proxy+InvocationHandler | net.sf.cglib.proxy.Enhancer |
| 方法调用 | 通过反射调用invoke() | 通过方法拦截器调用,性能更高 |
| 限制 | 只能代理接口中定义的方法 | final类无法代理,final/private方法无法增强 |
JDK动态代理实现示例:
// 1. 定义接口publicinterfaceUserService{voidsaveUser(Useruser);}// 2. 实现类publicclassUserServiceImplimplementsUserService{publicvoidsaveUser(Useruser){// 业务逻辑}}// 3. 实现InvocationHandlerpublicclassLogInvocationHandlerimplementsInvocationHandler{privateObjecttarget;publicLogInvocationHandler(Objecttarget){this.target=target;}@OverridepublicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{System.out.println("【前置】方法:"+method.getName()+" 开始执行");Objectresult=method.invoke(target,args);System.out.println("【后置】方法:"+method.getName()+" 执行完成");returnresult;}}// 4. 创建代理UserServicetarget=newUserServiceImpl();InvocationHandlerhandler=newLogInvocationHandler(target);UserServiceproxy=(UserService)Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),handler);proxy.saveUser(newUser());4.3 Spring如何选择代理方式?
Spring的代理选择策略:
| 场景 | 代理方式 |
|---|---|
| 目标类实现了接口 | JDK动态代理(默认) |
| 目标类未实现接口 | CGLIB动态代理 |
| 强制使用CGLIB | 设置@EnableAspectJAutoProxy(proxyTargetClass = true) |
💡Spring Boot的默认行为:Spring Boot从2.0开始,默认将
spring.aop.proxy-target-class设置为true,即优先使用CGLIB代理。这意味着即使你的类实现了接口,Spring Boot也会用CGLIB来代理,这样可以代理类中所有方法(而不仅仅是接口中定义的方法)。
强制使用CGLIB的配置方式:
@Configuration@EnableAspectJAutoProxy(proxyTargetClass=true)// 强制CGLIBpublicclassAppConfig{}# application.ymlspring:aop:proxy-target-class:true# Spring Boot 2.x+ 默认就是true4.4 Spring AOP vs AspectJ
很多开发者容易混淆Spring AOP和AspectJ,两者有本质区别:
| 对比维度 | Spring AOP | AspectJ |
|---|---|---|
| 织入时机 | 运行时动态代理 | 编译时或类加载时织入 |
| 实现方式 | 生成代理对象 | 直接修改字节码 |
| 连接点支持 | 仅方法调用 | 字段访问、构造器、静态代码块等 |
| 性能 | 略低(代理调用链) | 更高(直接执行增强后的字节码) |
| 使用复杂度 | 简单,与Spring无缝集成 | 需要AspectJ编译器(ajc) |
| 适用场景 | 轻量级、Spring Bean的方法增强 | 复杂AOP需求、非Spring管理的对象 |
💡一句话总结:Spring AOP是"运行时套壳子",AspectJ是"编译时改代码"。大多数场景下Spring AOP足够用,只有在需要代理非Spring Bean或需要字段级别拦截时,才需要AspectJ。
五、AOP代理创建流程(源码级)
5.1 三大核心步骤
Spring AOP代理对象的创建分为三个核心步骤:
5.2 源码核心类解析
Spring AOP的代理创建由AbstractAutoProxyCreator驱动,它是BeanPostProcessor的一个实现,在Bean生命周期中发挥作用。
核心类继承关系:
BeanPostProcessor(Bean生命周期扩展点) ↑ 实现 AbstractAutoProxyCreator(自动代理创建器基类) ↑ 继承 AbstractAdvisorAutoProxyCreator(增加Advisor处理能力) ↑ 继承 AnnotationAwareAspectJAutoProxyCreator(@AspectJ注解驱动,实际使用的实现类)核心方法postProcessBeforeInstantiation:
@OverridepublicObjectpostProcessBeforeInstantiation(Class<?>beanClass,StringbeanName){// 1. 检查是否应该跳过if(isInfrastructureClass(beanClass)||shouldSkip(beanClass,beanName)){this.advisedBeans.put(cacheKey,Boolean.FALSE);returnnull;}// 2. 获取自定义TargetSourceTargetSourcetargetSource=getCustomTargetSource(beanClass,beanName);if(targetSource!=null){// 3. 获取适用于该Bean的增强器Object[]specificInterceptors=getAdvicesAndAdvisorsForBean(beanClass,beanName,targetSource);// 4. 创建代理对象returncreateProxy(beanClass,beanName,specificInterceptors,targetSource);}returnnull;}代理创建流程:
- BeanPostProcessor机制:在Bean实例化之前,
AbstractAutoProxyCreator拦截Bean的创建过程 - 解析切面定义:扫描所有
@Aspect切面类,构建Advisor集合 - 匹配增强器:通过切点表达式判断当前Bean是否需要被代理
- 创建ProxyFactory:将目标对象、增强器等信息注入代理工厂
- 选择代理策略:根据目标类是否实现接口,选择JDK或CGLIB
- 生成代理对象:通过
ProxyFactory创建代理对象,替换容器中的原始Bean
六、AOP执行顺序
6.1 单个切面中五种通知的执行顺序
当一个切面中定义了多种通知时,执行顺序如下:
代码示例:
@Aspect@ComponentpublicclassLogAspect{@Around("execution(* com.example.service.*.*(..))")publicObjectaround(ProceedingJoinPointpjp)throwsThrowable{System.out.println("① @Around 前置:方法开始");Objectresult=pjp.proceed();// 执行目标方法System.out.println("④ @Around 后置:方法结束");returnresult;}@Before("execution(* com.example.service.*.*(..))")publicvoidbefore(){System.out.println("② @Before:前置通知");}@After("execution(* com.example.service.*.*(..))")publicvoidafter(){System.out.println("⑤ @After:后置通知(finally)");}@AfterReturning("execution(* com.example.service.*.*(..))")publicvoidafterReturning(){System.out.println("⑥ @AfterReturning:正常返回");}}执行结果:
① @Around 前置:方法开始 ② @Before:前置通知 —— 目标方法执行 —— ④ @Around 后置:方法结束 ⑤ @After:后置通知(finally) ⑥ @AfterReturning:正常返回6.2 多个切面的执行顺序
当多个切面作用于同一个目标方法时,通过@Order注解控制执行顺序:
| 规则 | 说明 |
|---|---|
@Order(数值) | 数值越小,优先级越高 |
| 优先级高的切面 | 外层(先执行前置,后执行后置) |
| 优先级低的切面 | 内层(后执行前置,先执行后置) |
多个切面嵌套执行顺序:
切面A(@Order(1),外层)@Around前置 ↓ 切面B(@Order(2),内层)@Around前置 ↓ 切面B @Before ↓ 目标方法执行 ↓ 切面B @Around后置 ↓ 切面B @After ↓ 切面A @Around后置 ↓ 切面A @After💡记忆口诀:前置通知"数值小先执行",后置通知"数值大先执行"——整体像一个"栈",先进后出。
七、AOP失效场景(高频面试题)
AOP基于动态代理实现,以下场景会导致AOP失效:
7.1 自调用(最经典的失效场景)
问题代码:
@ServicepublicclassUserService{@Transactional// ❌ 不会生效!publicvoidupdateUser(Useruser){// 更新用户逻辑}publicvoidupdateUserWithLog(Useruser){// 直接调用本类方法——绕过了代理对象!this.updateUser(user);// ❌ AOP失效logService.saveLog("更新用户");}}为什么失效?
this.updateUser()调用的是原始对象的方法,而不是代理对象的方法。Spring的AOP增强是通过代理对象实现的——只有通过代理对象调用方法,切面逻辑才会执行。
解决方案:
@ServicepublicclassUserService{@AutowiredprivateUserServiceself;// 注入自身(代理对象)@TransactionalpublicvoidupdateUser(Useruser){// 更新用户逻辑}publicvoidupdateUserWithLog(Useruser){self.updateUser(user);// ✅ 通过代理对象调用,事务生效}}或者使用AopContext.currentProxy():
((UserService)AopContext.currentProxy()).updateUser(user);7.2 其他常见失效场景
| 失效场景 | 原因 | 解决方案 |
|---|---|---|
| 非public方法 | 代理只能增强public方法 | 改为public |
| 异常被try-catch吞掉 | 通知无法捕获异常 | 在catch中重新抛出异常 |
| 内部方法调用(自调用) | 绕过代理对象 | 通过代理对象调用或拆分到不同类 |
| 方法被final修饰 | CGLIB无法重写final方法 | 移除final修饰 |
| 类被final修饰 | CGLIB无法继承final类 | 移除final修饰 |
八、避坑要点
| 错误/误区 | 后果 | 正确做法 |
|---|---|---|
| 认为AOP能代理所有方法 | private/final方法增强失败 | 只对public方法使用AOP |
| 在类内部调用自己的AOP方法 | 切面逻辑不执行(自调用) | 通过代理对象调用或拆分到不同类 |
| 混淆Spring AOP和AspectJ | 对AOP能力预期错误 | Spring AOP仅支持方法级别,运行时织入 |
忘记加@EnableAspectJAutoProxy | @Aspect不生效 | Spring Boot自动配置,无需手动添加 |
在@Around中忘记调用proceed() | 目标方法不执行 | 必须在@Around中调用pjp.proceed() |
| 认为JDK代理比CGLIB快 | 选错代理方式 | JDK代理有反射开销,CGLIB性能更好 |
九、面试高频考点
Q1:AOP的底层原理是什么?
AOP的底层是动态代理。Spring在运行时为目标Bean创建代理对象(JDK动态代理或CGLIB),将切面逻辑织入代理对象中。当调用目标方法时,实际调用的是代理对象的方法——代理对象先执行切面逻辑(通知),再调用原始对象的方法。
Q2:JDK动态代理和CGLIB有什么区别?
JDK动态代理基于接口,要求目标类实现至少一个接口,通过
Proxy和InvocationHandler生成代理。CGLIB基于继承,通过生成目标类的子类来实现代理,不需要接口。JDK只能代理接口中定义的方法,CGLIB可以代理类中所有非final方法。Spring Boot 2.x+默认使用CGLIB。
Q3:@Transactional为什么会失效?
最经典的是自调用失效——同一个类中,一个非事务方法调用本类的
@Transactional方法,调用的是原始对象的this引用,绕过了代理对象,事务不生效。此外,private方法、final方法、异常被try-catch吞掉、未启用事务管理也会导致失效。
Q4:@Around和@Before+@After有什么区别?
@Around是环绕通知,可以完全控制目标方法的执行——决定是否执行、何时执行、执行前后做什么。@Before+@After只能分别在方法前后执行逻辑,无法控制方法是否执行。@Transactional的底层就是@Around通知——开启事务→执行业务方法→根据结果提交或回滚。
Q5:Spring AOP和AspectJ有什么区别?
Spring AOP是运行时通过动态代理织入,仅支持方法级别的连接点。AspectJ是编译时或类加载时通过修改字节码织入,支持字段、构造器、静态代码块等更丰富的连接点。Spring AOP更简单易用,AspectJ功能更强大、性能更好。
🎤 面试官追问陷阱(加分题)
追问1:“CGLIB能代理final类吗?能代理final方法吗?”
👉都不能。CGLIB通过继承目标类生成子类来实现代理——
final类不能被继承,final方法不能被重写。所以如果你的类或方法被final修饰,AOP增强会失效。Spring会fallback到JDK动态代理吗?不会——如果类没有接口且是final的,代理创建会直接失败。
追问2:“Spring Boot默认用CGLIB,那如果一个类既实现了接口,又被final修饰了,会怎样?”
👉 这是个好问题。Spring Boot默认CGLIB,但
final类无法被CGLIB代理。此时Spring会自动降级到JDK动态代理——只要该类实现了至少一个接口。所以结果是:代理可以创建成功,但使用的是JDK方式。这说明Spring的代理选择策略是有"容错"机制的——CGLIB不行就换JDK。
追问3:“@Around通知中调用了pjp.proceed()两次,会怎样?”
👉 目标方法会被执行两次。这在某些场景下是故意的(比如重试机制),但大多数情况下是bug。每次
proceed()都会重新执行目标方法及其嵌套的切面逻辑。所以如果你在@Around中不小心写了两次proceed(),事务可能会被开启两次、日志会记录两遍。
十、练习题
简答题:JDK动态代理和CGLIB动态代理在实现原理上有什么本质区别?各自的使用条件是什么?
代码题:编写一个
@Around环绕通知,统计目标方法的执行时间,并在方法执行前后打印日志。分析题:某项目中
@Transactional注解在UserService.updateUser()方法上不生效,但OrderService.createOrder()方法上的@Transactional正常。updateUser()是被UserService内部的另一个方法调用的。请分析原因并给出解决方案。
📊 你的学习进度
- 当前:第75篇 / 共108篇 ·进阶篇:Spring全家桶(第73~82篇)
- ✅ 已完成:基础篇44篇 + 第45~75篇
- 📖 正在学:第75篇
- ⏳ 待学习:第76~108篇
👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇
👉 下一篇文章预告
🚀下一篇:《第76篇:Spring事务》
内容简介:声明式事务
@Transactional原理、事务传播行为(REQUIRED/REQUIRES_NEW/NESTED等7种)、隔离级别、事务失效场景深度剖析、编程式事务。
👉Spring全家桶专题持续推进,拿下事务管理!
📌《Java 100 天进阶之路 | 从入门到上岗就业》每天一篇,建议收藏 + 关注,一起100天拿offer!
👉 点击关注我,更新后第一时间收到推送!