人人都会AI编程

6.2 声明式事务实现原理:AOP 代理 + 事务拦截器

更新时间:2026-07-10

上一节我们掌握了 @Transactional 的实用规则和失效场景,但你是否想过:仅仅一个注解,是如何让一个普通方法自动拥有开启、提交、回滚事务的能力的?答案就藏在 AOP 代理事务拦截器 的协作之中。理解这套原理,不仅有助于精准定位事务失效问题,更能让你对 Spring 的整体架构形成贯穿性的认知。

6.2.1 整体流程概览

当我们在某个 Bean 的方法上标注 @Transactional 时,Spring 会在容器初始化阶段为这个 Bean 生成一个 AOP 代理对象。此后,所有对该 Bean 的外部调用都会先经过代理,代理内部再委托给 TransactionInterceptor(事务拦截器)执行事务逻辑,最后才到达真正的目标方法。

整个过程可以概括为三个关键步骤:

  1. 代理创建:Spring 检测到 Bean 的方法包含事务注解,为该 Bean 创建动态代理。
  2. 拦截请求:调用代理的方法时,事务拦截器拦截请求,在目标方法的前后执行事务控制代码。
  3. 事务管理:拦截器借助 PlatformTransactionManager 开启、提交或回滚事务。

下图展示了一个典型的调用链路:

调用者 → 代理对象(Proxy) → TransactionInterceptor. invoke()
                            ├─ 开启事务
                            ├─ 执行目标方法
                            └─ 提交/回滚事务

接下来,我们逐步拆解每个环节。

6.2.2 第一步:Spring 如何决定创建代理

Spring 对事务注解的支持是通过 @EnableTransactionManagement 开启的(Spring Boot 自动配置已默认启用)。该注解会向容器中注册一个关键的 Bean 后处理器:InfrastructureAdvisorAutoProxyCreator(或者其具体实现 AnnotationAwareAspectJAutoProxyCreator)。这个处理器在 Bean 初始化时会检查每个 Bean 是否需要被增强。

判断条件:如果 Bean 中有一个或多个方法被 @Transactional 标记,或者 Bean 被标记了 @Transactional,Spring 就会为该 Bean 创建代理对象。注意,这要求 Bean 是通过 Spring 容器获取的,并且调用方式是从外部通过代理进行的(如果类内部调用 this.method() 会绕过代理,这也是事务失效的根源)。

代理类型

  • 如果目标类实现了至少一个接口,默认使用 JDK 动态代理(基于接口)。
  • 如果没有实现接口,或者显式设置 proxyTargetClass = true,则使用 CGLIB 代理(通过生成子类)。
  • Spring Boot 2.x 起,AOP 默认配置已将 proxyTargetClass 设为 true,因此大部分情况下我们获得的是 CGLIB 代理。

6.2.3 第二步:TransactionInterceptor —— 事务拦截器的核心

当事务代理生成后,每个方法调用都会被 TransactionInterceptor 拦截。它是整个声明式事务的大脑,实现了 MethodInterceptor 接口,其 invoke() 方法承载着事务控制的主体逻辑。

简化后的核心源码流程如下(基于 Spring Framework 6.x 的 TransactionInterceptor):

public Object invoke(MethodInvocation invocation) throws Throwable {
    // 1. 获取目标类(可能是代理的目标类)
    Class<?> targetClass = (invocation.getThis() != null) 
            ? AopUtils.getTargetClass(invocation.getThis()) : null;

    // 2. 调用事务管理器的核心方法
    return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}

重点在 invokeWithinTransaction() 方法内部,其逻辑可分解为以下几个阶段:

(1)获取事务属性

首先从方法或类上解析 @Transactional 的各项配置(传播行为、隔离级别、超时、只读、回滚规则等),组装成一个 TransactionAttribute 对象。

TransactionAttributeSource tas = getTransactionAttributeSource();
TransactionAttribute txAttr = (tas != null) 
        ? tas.getTransactionAttribute(method, targetClass) : null;

(2)获取事务管理器

根据 @Transactional 配置的事务管理器标识(valuetransactionManager 属性),或者默认的类型匹配,从容器中找到对应的 PlatformTransactionManager。通常数据源相关的 DataSourceTransactionManager 是被自动注入的。

(3)开启事务

调用事务管理器的 getTransaction(TransactionDefinition) 方法,该方法内部会根据传播行为决定是否创建新事务:

  • 如果传播行为是 REQUIRED 且当前没有事务,则创建一个新事务。
  • 如果是 REQUIRED 且存在事务,则加入已有事务。
  • 如果是 REQUIRES_NEW,则挂起当前事务并开启新事务。

这个过程最终返回一个 TransactionInfo 对象,其中封装了事务状态,并与当前线程绑定。

TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);

(4)执行目标方法

在事务上下文建立后,拦截器调用 invocation.proceed() 来执行真正的业务方法。此时发生的任何数据库操作都会在这个事务范围内进行。

(5)处理异常或正常返回

目标方法执行完毕后,拦截器根据返回结果或抛出的异常类型,决定事务的最终结局:

  • 正常返回:调用 commitTransactionAfterReturning(txInfo) 提交事务。
  • 抛出异常:检查异常类型是否匹配回滚规则(默认 RuntimeExceptionError 回滚),如果是,则调用 completeTransactionAfterThrowing(txInfo, ex) 进行回滚;否则提交事务。
  • 如果业务异常被 @Transactional(noRollbackFor = ...) 显式排除,即使抛出该异常也不会回滚。

(6)清理事务上下文

无论提交或回滚,最终都需要清理 TransactionInfo 和线程绑定的资源,恢复挂起的事务(如果存在传播行为导致的挂起)。

6.2.4 一个完整的调用过程(结合实例)

假设我们有以下简单服务:

@Service
public class OrderService {
    
    @Transactional
    public void placeOrder(Order order) {
        orderDao.insert(order);
        inventoryService.reduceStock(order.getProductId(), order.getQuantity());
    }
}

当另一个 Bean(例如 Controller)调用 orderService.placeOrder(order) 时,实际执行流程如下:

  1. Controller 通过 @Autowired 持有 OrderServiceCGLIB 代理对象
  2. 调用代理对象的 placeOrder() 方法,CGLIB 代理将调用转交给 TransactionInterceptorinvoke()
  3. TransactionInterceptor 解析到 placeOrder 具有 @Transactional,获取默认事务管理器(例如 DataSourceTransactionManager)。
  4. 事务管理器检查当前线程无事务,根据 REQUIRED 传播行为创建一个新数据库连接,并将自动提交设为 false,开启事务,返回事务状态对象。
  5. 拦截器调用目标方法 placeOrder()(真正的业务对象),执行插入订单和扣减库存。
  6. 执行完毕无异常,拦截器调用 commitTransactionAfterReturning(),事务管理器对数据库连接执行 commit()
  7. 清理资源,恢复连接自动提交状态,解除线程绑定。

如果在扣减库存时抛出 InsufficientStockException(一个 RuntimeException),则拦截器捕获到异常,判断符合回滚规则,调用 rollback() 使前面的插入操作一同回滚。

6.2.5 ThreadLocal 与事务同步

事务能正确地在同一个线程中传播,离不开 ThreadLocal。Spring 使用 TransactionSynchronizationManager 这个工具类,将 数据库连接(资源)事务状态 绑定到当前线程。

  • 当调用 DataSourceTransactionManager.doBegin() 时,会从数据源获取一个连接,并通过 TransactionSynchronizationManager.bindResource(dataSource, connection) 将连接存入当前线程的 ThreadLocal 中。
  • 同一个事务里后续的数据库操作(例如 JdbcTemplate)在获取连接时会先检查当前线程是否有绑定的资源,有则直接复用,从而保证所有操作共享同一个连接和事务。
  • 当事务结束(提交或回滚)后,连接被解除绑定并归还连接池。

这也是为什么声明式事务要求各数据库操作处于同一线程:跨线程调用时,新线程拿不到原来绑定的连接,事务自然无法传递。

6.2.6 理解原理的实际价值

清楚事务的 AOP 代理和拦截器机制后,许多之前“莫名其妙”的问题就有了清晰的解释:

  • 为何类内部调用会失效:因为调用绕过了代理,直接调用 this.method() 不会触发拦截器。
  • 为何非 public 方法可能不生效:CGLIB 可以代理 protected 和包可见的方法,但 Spring 默认的事务属性源只解析 public 方法(可通过自定义配置扩展,但不推荐)。
  • 为何需要外部调用代理对象:事务拦截器只存在于代理链路中,不在目标对象内部。
  • 为何事务传播能感知已有事务:通过 TransactionSynchronizationManager 存储当前线程事务,不论调用多少层代理,只要控制权在同一个线程内,就能获取到当前事务。

掌握这些原理之后,你就能在设计服务边界和调用链路时,有意识地避免踩坑,并充分发挥声明式事务的威力。下一节我们将进一步探讨事务传播行为的细节以及如何在不同业务场景下做出合理选择。