【Spring】 AOP 核心原理,与声明式事务传播机制
一、什么是 AOP
AOP(Aspect Oriented Programming,面向切面编程)
核心思想
在不修改原有业务代码的情况下,对方法进行统一增强。
例如:
- 日志记录;权限校验;事务管理;性能统计;异常处理
这些都属于“公共功能”,不应该和业务代码耦合在一起。
为什么需要 AOP
传统开发:
public void save(){
// 日志
// 权限校验
// 事务
// 业务代码
}问题:
代码重复;耦合度高;不方便维护;修改公共逻辑需要改很多地方
AOP 的做法:把公共逻辑抽取出来,在方法执行前后动态置入。
二、AOP 的核心概念
1.五个概念
术语 含义 用人话解释 Joinpoint (连接点) 程序中可植入增强的代码点 具体来说,就是系统中的各个方法。 PointCut (切入点) 真正决定要被拦截、增强的方法集合 也就是我们定义的拦截规则(如某个包下的所有方法)。 Advice (通知/增强) 切面主要做的事情 是具体的业务代码,比如“在方法前记日志”、“在方法后提交事务”。 Aspect (切面) 切入点 + 通知 的集合 一个切面定义了:在哪里(PointCut),做什么(Advice)。 Proxy (代理) Spring AOP 底层通过代理实现;真正执行的是代理对象不是原对象 代理对象包装了原有业务对象,并织入了增强逻辑。 2.通知类型
1. @Before()前置通知
方法执行前执行;如权限校验,参数校验
2. @After 后置通知
方法执行后执行;无论是否异常都会执行,类似finally
3. @AfterReturning 返回通知
方法正常返回后执行
4. @AfterThrowing 异常通知
方法发生异常时执行
5. Around 环绕通知(最重要)
可以:
- 方法前增强
- 方法后增强
- 控制方法是否执行
- 修改返回值
- 统计耗时
3.Around 环绕通知举例
// @Aspect 表明这是一个切面类 // @Order(1) 有多个切面时,数字越小优先级越高,最先被执行 @Aspect @Order(1) @Component // 必须交给 Spring 容器管理 public class TimeAspect { @Around(⭐"execution(* com.demo.controller.*.*(..))") public ⭐Object around(ProceedingJoinPoint pjp) ⭐throws Throwable { // 1. 方法执行前,记录开始时间 long start = System.currentTimeMillis(); // 2. 执行目标方法 ⭐Object result = pjp.proceed(); // 3. 方法执行后,记录结束时间 long end = System.currentTimeMillis(); System.out.println("耗时:" + (end - start)); return result; } }⭐execution 切点表达式:execution(访问修饰符 返回值 包名.类名.方法名(参数))
*匹配任意字符
- 任意返回值
- service 包下任意类
- 任意方法
- 任意参数
..任意层级例如com.demo..
⭐Around 返回 Object:因为要兼容所有方法返回值
除了 @Around 类型外,其他通知(Before, After 等)定义时建议设为
void(不写返回值),即使写了,Spring 也会忽略,不会被作为业务返回值传给调用方⭐
pjp.proceed()必须手动用throws Throwable抛出,不能在这里try-catch将异常截获,否则原本的异常处理逻辑(如@AfterThrowing或统一异常处理)将失效⭐ProceedingJoinPoint:pjp.proceed()作用是执行目标方法,除环绕通知外的其他通知类型会自动执行目标方法,但是环绕通知必须手动执行
♦ Around 做什么了:记录耗时
♦@Pointcut(切点复用)
//定义切点 @Pointcut("execution(* com.demo.service.*.*(..))") public void pt(){} //使用切点 @Before("pt()")4.AOP 执行流程
Spring 启动时:
- 扫描 Bean
- 找到带有
@Aspect的切面类- 找到切点表达式
- 判断哪些 Bean 需要增强
- 为这些 Bean 创建代理对象
- 调用方法时执行增强逻辑
5.Spring AOP 底层原理
代理对象是在程序运行期间动态生成的,将方法调用从客户端拦截下来,由代理对象执行一些额外的逻辑(增强),然后再选择是否调用目标对象(Target)的原始方法
静态代理vs 动态代理
静态代理:手动为每个目标类编写一个代理类,编译为
.class文件。在代码运行前就已经确定。缺点:随着业务类增加,代理类数量暴增,难以维护。动态代理(Spring AOP 采用):在程序运行期间,由框架在内存中动态生成代理对象。不需要手动编写
.class文件。动态代理动态代理先生成“类(字节码)”,再根据这个类去创建“Bean 实例”
(1)JDK 动态代理
目标类必须实现类接口,代理类动态创建一个实现相同接口的类
(2)CGLIB 动态代理
通过继承目标类生成子类代理,不需要接口;Spring 默认优先使用 JDK,没有接口时使用 CGLIB
Spring 动态代理的决策逻辑(常问):
如果目标对象实现了接口,默认使用 JDK 动态代理。
如果目标对象未实现接口,则必须使用 CGLIB 动态代理。
【注】:Spring Boot 2.x 后,默认配置可能偏向全员使用 CGLIB 以避免因为是否实现接口导致的行为不一致。
派系 实现机制 优缺点/适用场景 JDK 动态代理 基于接口实现。代理类与目标类实现相同的接口。通过 InvocationHandler和Proxy类实现。优点:速度快。缺点:只能代理实现了接口的类。若无接口,无法使用。 CGLIB 动态代理 基于继承实现。生成的代理类是目标类的子类。使用底层字节码技术生成。 优点:可以代理没有接口的类。性能在某些场景下优于 JDK。缺点:不能代理 final方法。(final类不能被继承)
三、Spring 事务
事务是保证数据完整性和一致性的核心手段。在 Spring 中,我们通过简单的一个
@Transactional注解即可实现这一复杂功能,其底层同样是基于 AOP 代理机制1. 事务的核心概念(Transaction)
定义:将一组逻辑上相关的操作包装成一个原子单位,这些操作要么全部成功,要么全部失败。
底层支撑:Spring 本身并不管理事务,而是提供了一套一致的接口,具体实现依赖底层的数据库(如 MySQL 的 InnoDB 引擎支持事务,MyISAM 不支持)。
2. 声明式事务 vs 编程式事务
编程式事务:在业务代码中手动写
commit()、rollback()逻辑。缺点:侵入性强,难以维护。声明式事务(AOP 代理形式):在方法或类上加上
@Transactional注解。
底层原理:Spring 产生目标类的代理对象。当调用方法时,代理对象先开启事务,然后执行业务逻辑,如果没有发生异常,则代理提交事务;如果发生特定异常,则代理回滚事务。
3. @Transactional 的核心配置要点
作用域:可以作用在方法上,也可以作用在类上(类下所有方法都生效)。
底层 AOP 切点实现思路:
切点:所有加了
@Transactional的方法。通知逻辑(Advice):代理开启事务(TransactionStatus状态) -> 执行原业务 -> 成功则代理提交 / 失败则代理回滚。
4. 事务回滚规则设置
默认回滚异常:Spring 只对
RuntimeException(运行时异常)和Error进行回滚。注意!:
IOException(检查异常)、FileNotFoundException等默认是不回滚的!必须手动配置。解决方案:
@Transactional(rollbackFor = Exception.class):推荐全员配置,拦截所有类型的异常。或者手动
try-catch异常并在代码中调用底层 API 手动回滚,但这就变成了编程式事务。5. 事务传播属性(Propagation - 面试必问)
当一个事务方法调用另一个事务方法时,事务应该如何处理?
propagation = Propagation.REQUIRED(默认值):外层方法有事务,就加入;如果没有,外层方法自己新建一个。
propagation = Propagation.REQUIRES_NEW:无论外层有没有事务,自己都必须新建一个事务,并且把外层事务挂起(如果是外层事务调入,两个事务互不影响,外层异常回滚,不影响子事务)。【注】:这种策略在处理写独立审计日志、发放奖品等逻辑时非常常用。
四、Spring 事务传播机制
为了方便理解,我们统一设定一个代码调用背景:
方法 A 开启了事务(或没有)。
方法 A 内部调用了方法 B(方法 B 配置了不同的传播机制)
传播行为 (Propagation) 外层A有事务时 外层A无事务时 说明 REQUIRED(默认)加入A事务 新建事务 融合在一起,同生共死 REQUIRES_NEW挂起A,新建事务 新建事务 彼此独立,互不干扰 NESTED嵌套子事务 (Savepoint) 新建事务 A崩全崩,B崩A可不崩 SUPPORTS加入A事务 非事务运行 随遇而安 NOT_SUPPORTED挂起A,非事务运行 非事务运行 拒绝事务,强制裸奔 MANDATORY加入A事务 抛出异常 必须有靠山,没靠山就罢工 NEVER抛出异常 非事务运行 见事务就见光死
同类方法调用失效问题
如果方法 A 和方法 B 在同一个 Service 类里,方法 A(REQUIRED)调用方法 B(REQUIRES_NEW),B 的新事务会生效吗?
答案:不会生效! 因为 Spring 事务基于 AOP 动态代理。类内部的直接方法调用(
this.B())指向的是真实目标对象,而不是外面的代理对象,导致 B 的@Transactional注解直接被忽略,B 会直接沿用 A 的事务(等同于 REQUIRED)。根本原因:
this.B()是一次普通的 JVM 内部方法调用,根本没有走 Spring 代理对象的业务分发链路。既然没经过代理,加在B()上的所有 AOP 注解(事务、日志、权限)自然全部失效。流程:外部调用(如 Controller 调用
userService.A())---------调用首先走到UserService代理对象---------代理对象触发开启事务---------代理对象内部调用target.A()---------A()方法内正在执行的是目标对象的代码---------A调用this.B---------调用的是真实目标对象@Service public class UserService { @Transactional public void A() { // 外部调用进来,事务成功开启 // 此时 code 在 Target 对象内部运行 this.B(); // 相当于 target.B(); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void B() { // 因为是 target 直接调用的,没有经过外面的 Proxy, // 代理对象身上的 AOP 增强(新开事务)直接被绕过了! } }解决办法:使用
AopContext.currentProxy()获取当前代理对象去调用 B,或者将 B 拆分到不同的 Service 类中。
①核心大三策略(最常用)
这是笔记中重点标记、面试出镜率高达 90% 的三种机制。
1. REQUIRED(必须有事务)—— 默认策略
人话解释:有同享,无自建。
业务逻辑:
若 A 有事务,B 就会加入 A 的事务,变成同一个事务。
若 A 没有事务,B 就会自己新建一个事务。
致命痛点:由于 A 和 B 在同一个事务中,其中任何一处报错导致回滚,全盘回滚(即便 B 报错被 A 用
try-catch捕获了,也会因为全局事务被标记为rollback-only而引发异常回滚)。2. REQUIRES_NEW(必须新事务)
人话解释:不管你有没有,我都要住单间。
业务逻辑:
不管 A 有没有事务,B 都会开启一个全新的事务。
若 A 有事务,A 会先挂起(Suspend),等待 B 的事务运行完,A 再继续运行。
影响结果:A 和 B 是两个独立的事务。B 报错回滚不会影响 A 的正常提交(前提是 A 捕获了 B 的异常);同理,A 后面报错回滚,也绝对不会影响已经提交的 B 事务。
典型场景:记录审计日志、发放独立优惠券。无论核心下单业务成功与否,日志必须入库。
3. NESTED(嵌套事务)
业务逻辑:
若 A 有事务,B 会在 A 的事务内部设立一个 保存点(Savepoint)。
关键特质:
局部回滚:如果 B 报错回滚,B 只会回滚到 Savepoint,不影响 A 事务的执行(A 捕获异常后可以继续提交)。
全局连坐:如果 A 报错回滚,由于 B 是嵌套在 A 里面的,AB 会全部回滚。
② 挂载与不支持策略(3个不常用)
这三个策略通常用于优化特定场景下的性能(如纯查询逻辑)。
4. SUPPORTS(支持事务)
业务逻辑:
A 有事务,B 就加入 A 的事务。
A 没有事务,B 就以非事务(普通)方式运行。
典型场景:只读查询方法。
5. NOT_SUPPORTED(不支持事务)
业务逻辑:
始终以非事务方式运行。
即使 A 有事务,B 也会把 A 的事务挂起,等自己用非事务方式裸奔完,再恢复 A 的事务。
6. MANDATORY(强制要求有事务)
业务逻辑:
极度傲娇。A 必须有事务,B 才会加入。
若 A 没有事务,B 直接抛出异常(
IllegalTransactionStateException)。③ 绝对禁区
7. NEVER(禁用事务)
业务逻辑:
始终以非事务方式运行。
如果 A 有事务,B 逮到直接抛出异常。
更多推荐




所有评论(0)