一、什么是 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 启动时:

  1. 扫描 Bean
  2. 找到带有 @Aspect 的切面类
  3. 找到切点表达式
  4. 判断哪些 Bean 需要增强
  5. 为这些 Bean 创建代理对象
  6. 调用方法时执行增强逻辑

5.Spring AOP 底层原理

代理对象是在程序运行期间动态生成的,将方法调用从客户端拦截下来,由代理对象执行一些额外的逻辑(增强),然后再选择是否调用目标对象(Target)的原始方法

静态代理vs 动态代理

  • 静态代理:手动为每个目标类编写一个代理类,编译为 .class 文件。在代码运行前就已经确定。缺点:随着业务类增加,代理类数量暴增,难以维护。

  • 动态代理(Spring AOP 采用):在程序运行期间,由框架在内存中动态生成代理对象。不需要手动编写 .class 文件。

  • 动态代理动态代理先生成“类(字节码)”,再根据这个类去创建“Bean 实例”

(1)JDK 动态代理

目标类必须实现类接口,代理类动态创建一个实现相同接口的类

(2)CGLIB 动态代理

通过继承目标类生成子类代理,不需要接口;Spring 默认优先使用 JDK,没有接口时使用 CGLIB

Spring 动态代理的决策逻辑(常问)

  1. 如果目标对象实现了接口,默认使用 JDK 动态代理

  2. 如果目标对象未实现接口,则必须使用 CGLIB 动态代理

  3. 【注】:Spring Boot 2.x 后,默认配置可能偏向全员使用 CGLIB 以避免因为是否实现接口导致的行为不一致。

派系 实现机制 优缺点/适用场景
JDK 动态代理 基于接口实现。代理类与目标类实现相同的接口。通过 InvocationHandlerProxy 类实现。 优点:速度快。缺点:只能代理实现了接口的类。若无接口,无法使用。
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 逮到直接抛出异常

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐