一、什么是 AOPAOPAspect Oriented Programming面向切面编程核心思想在不修改原有业务代码的情况下对方法进行统一增强。例如日志记录权限校验事务管理性能统计异常处理这些都属于“公共功能”不应该和业务代码耦合在一起。为什么需要 AOP传统开发public void save(){// 日志// 权限校验// 事务// 业务代码}问题代码重复耦合度高不方便维护修改公共逻辑需要改很多地方AOP 的做法把公共逻辑抽取出来在方法执行前后动态置入。二、AOP 的核心概念1.五个概念术语含义用人话解释Joinpoint (连接点)程序中可植入增强的代码点具体来说就是系统中的各个方法。PointCut (切入点)真正决定要被拦截、增强的方法集合也就是我们定义的拦截规则如某个包下的所有方法。Advice (通知/增强)切面主要做的事情是具体的业务代码比如“在方法前记日志”、“在方法后提交事务”。Aspect (切面)切入点 通知 的集合一个切面定义了在哪里PointCut做什么Advice。Proxy (代理)Spring AOP 底层通过代理实现真正执行的是代理对象不是原对象代理对象包装了原有业务对象并织入了增强逻辑。2.通知类型1. Before()前置通知方法执行前执行如权限校验参数校验2. After 后置通知方法执行后执行无论是否异常都会执行类似finally3. 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或统一异常处理将失效⭐ProceedingJoinPointpjp.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 实例”1JDK 动态代理目标类必须实现类接口代理类动态创建一个实现相同接口的类2CGLIB 动态代理通过继承目标类生成子类代理不需要接口Spring 默认优先使用 JDK没有接口时使用 CGLIBSpring 动态代理的决策逻辑常问如果目标对象实现了接口默认使用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 类里方法 AREQUIRED调用方法 BREQUIRES_NEWB 的新事务会生效吗答案不会生效因为 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 逮到直接抛出异常。
【Spring】 AOP 核心原理,与声明式事务传播机制
发布时间:2026/5/21 21:02:56
一、什么是 AOPAOPAspect Oriented Programming面向切面编程核心思想在不修改原有业务代码的情况下对方法进行统一增强。例如日志记录权限校验事务管理性能统计异常处理这些都属于“公共功能”不应该和业务代码耦合在一起。为什么需要 AOP传统开发public void save(){// 日志// 权限校验// 事务// 业务代码}问题代码重复耦合度高不方便维护修改公共逻辑需要改很多地方AOP 的做法把公共逻辑抽取出来在方法执行前后动态置入。二、AOP 的核心概念1.五个概念术语含义用人话解释Joinpoint (连接点)程序中可植入增强的代码点具体来说就是系统中的各个方法。PointCut (切入点)真正决定要被拦截、增强的方法集合也就是我们定义的拦截规则如某个包下的所有方法。Advice (通知/增强)切面主要做的事情是具体的业务代码比如“在方法前记日志”、“在方法后提交事务”。Aspect (切面)切入点 通知 的集合一个切面定义了在哪里PointCut做什么Advice。Proxy (代理)Spring AOP 底层通过代理实现真正执行的是代理对象不是原对象代理对象包装了原有业务对象并织入了增强逻辑。2.通知类型1. Before()前置通知方法执行前执行如权限校验参数校验2. After 后置通知方法执行后执行无论是否异常都会执行类似finally3. 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或统一异常处理将失效⭐ProceedingJoinPointpjp.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 实例”1JDK 动态代理目标类必须实现类接口代理类动态创建一个实现相同接口的类2CGLIB 动态代理通过继承目标类生成子类代理不需要接口Spring 默认优先使用 JDK没有接口时使用 CGLIBSpring 动态代理的决策逻辑常问如果目标对象实现了接口默认使用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 类里方法 AREQUIRED调用方法 BREQUIRES_NEWB 的新事务会生效吗答案不会生效因为 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 逮到直接抛出异常。