【重写SpringFramework】声明式事务下:事务拦截器(chapter 4-6)

158 阅读16分钟

本文首发微信公众号【Java编程探微】

1. 前言

上一节介绍了如何通过 Spring AOP 来构建事务切面,具体来说需要定义三个组件,即切点、拦截器和 Advisor,然后声明 @Transactional 注解就可以标记一个事务方法。当调用事务方法时,代理对象通过 TransactionInterceptor 来执行事务相关操作。这样做的好处是将业务逻辑和事务操作解耦,用户只需要关心具体业务的实现即可。那么,事务拦截器是如何发挥作用的,本节将进行详细的讨论。

2. 事务信息

2.1 TransactionInfo

TransactionInfoTransactionAspectSupport 的内部类,对于每个事务方法来说,都会生成对应的 TransactionInfo 实例,并根据事务方法的调用情况构建一条事务链。我们说 Spring 事务是横向结构的,关键就在于事务链。

  • transactionManager:事务管理器,每个事务方法可能使用不同的事务管理器
  • transactionAttribute:事务注解的相关属性
  • joinPointIdentification:连接点(方法)标识,形式为全类名+方法名
  • transactionStatus:事务状态,负责管理事务的相关操作
  • oldTransactionInfo:指向上一个事务方法,构成链表结构,相当于指针
protected final class TransactionInfo {
    private final PlatformTransactionManager transactionManager;
    private final TransactionAttribute transactionAttribute;
    private final String joinPointIdentification;
    private TransactionStatus transactionStatus;
    private TransactionInfo oldTransactionInfo;
}

2.2 组成结构

前几节介绍了很多与事务相关的类,再加上 TransactionInfo,我们来对这些类的功能做一个梳理,从整体的角度观察如何来描述声明式事务。

  • DataSourceTransactionObject:表示一个事务对象。持有一个 ConnectionHolder 实例,用于执行最底层的提交、回滚等操作。
  • TransactionAttribute:事务属性(定义),表示事务注解属性。对于一个事务方法来说,必须声明 @Transactional 注解,这是必要条件。但事务方法是否有效则另当别论,因而又是非充分条件。
  • TransactionStatus:事务状态,表示如何对事务方法进行操作。transaction 字段表示一个事务对象,可以为空,说明该方法以无事务的方式运行。
  • TransactionInfo:事务信息,完整地描述一个事务方法。最关键的是通过链表结构,将所有的事务方法串联起来,构成了一个事务链。

6.1 TransactionInfo组成结构.png

综上所述,TransactionInfo 完整地描述了一个事务方法,包括静态的编译信息和动态的运行时信息。首先提取事务方法的静态编译信息,也就是 @Transactional 注解的属性,然后解析为 TransactionAttribute。其次,根据运行时的调用情况动态地确定事务状态,TransactionStatus 告诉事务方法应该如何执行,包括是否使用事务,事务有没有超时,应该提交还是回滚等。概括来讲,事务的属性是编译时确定的,事务的状态是运行时生成且不断变化的。

3. TransactionAspectSupport

3.1 基本属性

TransactionInterceptor 参与了事务 AOP 的构建,具体的拦截逻辑是由父类 TransactionAspectSupport 完成的,主要的属性如下:

  • transactionManager:默认的事务管理器,作为兜底的选项,可以为空
  • transactionManagerCache:事务管理器的缓存,优先从容器中查找事务管理器实例,然后缓存起来
  • transactionInfoHolder:线程绑定的事务信息,始终指向当前执行的事务方法(尤其重要)
  • transactionAttributeSource:用于查找事务方法对应的注解属性
public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
    //默认的事务管理器,可空
    private PlatformTransactionManager transactionManager;
    //查找事务方法的注解信息
    private TransactionAttributeSource transactionAttributeSource;
    //当前线程存储的事务信息
    private static final ThreadLocal<TransactionInfo> transactionInfoHolder = new NamedThreadLocal<>("tx");
    //事务管理器缓存
    private final ConcurrentMap<Object, PlatformTransactionManager> transactionManagerCache = new ConcurrentHashMap<>(4);
}

3.2 构建事务链

TransactionAspectSupport 的工作原理是通过 AOP 进行拦截,在执行每个事务方法的过程中,为其生成相应的 TransactionInfo 对象。如果之前存在事务方法,则让当前方法的 TransactionInfo 指向上一个方法的 TransactionInfo,从而构成一条事务链。如下图所示:

  • 调用 A 方法,创建 TransactionInfo 实例,TransactionAspectSupporttxInfoHolder 字段指向 A 方法的事务信息。
  • 调用 B 方法,创建 TransactionInfo 实例,并指向 A 方法对应的 TransactionInfoTransactionAspectSupporttxInfoHolder 字段转而指向 B 方法的事务信息。

6.2 事务方法执行原理.png

以此类推,当调用 C 方法时,重复 B 方法的操作流程即可。当 C 方法执行完毕,C 方法对应的 TransactionInfo 会从事务链中移除,然后继续执行 B 方法的剩余逻辑。直到 A 方法执行完毕,整条事务链都不复存在,此时所有的事务都得到了执行。

4. 拦截方法详解

4.1 主流程

invokeWithinTransaction 方法定义了事务拦截的主体流程,从全局来看,这是一个典型的环绕通知。开启事务是前置通知,回滚事务是异常通知,清除事务信息以及提交事务是返回通知。先来看准备工作,首先尝试获取方法上的事务属性,如果不存在,说明方法上没有声明 @Transactional 注解,也就不会当做事务方法处理。然后获取方法标识,形式为全类名 + 方法名。接下来的操作分为四步:

  1. 获取事务管理器,不同的事务管理器可能指向不同的数据源,这对于 SQL 操作至关重要。
  2. 获取 TransactionInfo 对象,与当前方法一一对应,如有必要则尝试开启事务。
  3. 如果业务方法抛出异常,则进入异常处理流程。
  4. 如果业务方法正常执行,进入提交事务流程。
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, InvocationCallback invocation) throws Throwable {
    //事务属性如果为空,说明目标方法没有声明事务注解
    final TransactionAttribute txAttr = transactionAttributeSource.getTransactionAttribute(method, targetClass);
    //方法标识
    final String identification = ClassUtils.getQualifiedMethodName(method, targetClass);
    //1. 获取事务管理器
    final PlatformTransactionManager tm = determineTransactionManager(txAttr);

    //2. 开启事务
    TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, identification);
    Object retVal;

    try {
        //调用目标方法
        retVal = invocation.proceedWithInvocation();
    } catch (Throwable ex) {
        //3. 异常处理
        completeTransactionAfterThrowing(txInfo, ex);
        throw ex;
    } finally {
        //清除事务信息
        cleanupTransactionInfo(txInfo);
    }
    //4. 提交事务
    commitTransactionAfterReturn(txInfo);
    return retVal;
}

4.2 获取事务管理器

determineTransactionManager 方法的作用是尝试获取事务管理器,涉及两方面的操作,一是缓存的处理,二是从多个事务管理器中进行选择。具体来说,可以分为三步:

  1. 如果事务属性为空,返回手动设置的事务管理器。一般来说,不会直接设置事务管理器,因此返回的事务管理器为 null。一旦事务对象不存在,后续事务相关的操作都不会执行,也就是说仅作为普通方法执行。

  2. 如果事务属性的 qualifier 字段不为空,则从 Spring 容器中获取指定名称的事务管理器。

  3. 先尝试获取手动设置的事务管理器,如果不存在则获取默认的事务管理器。此时仍可能存在多个事务管理器,默认的是指主要的(primary)那个。

protected PlatformTransactionManager determineTransactionManager(TransactionAttribute txAttr) {
    //1. 没有事务属性则返回手动设置的事务管理器
    if (txAttr == null || this.beanFactory == null) {
        return getTransactionManager();
    }

    //2. 获取指定名称的事务管理器,即@Transactional的value属性
    String qualifier = txAttr.getQualifier();
    if(StringUtils.hasText(qualifier)){
        PlatformTransactionManager txManager = this.transactionManagerCache.get(qualifier);
        if (txManager == null) {
            txManager = this.beanFactory.getBean(qualifier, PlatformTransactionManager.class);
            this.transactionManagerCache.putIfAbsent(qualifier, txManager);
        }
        return txManager;
    }

    //3. 获取默认的事务管理器(qualifier未设置的情况)
    PlatformTransactionManager defaultTransactionManager = getTransactionManager();
    if(defaultTransactionManager == null){
        defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
        if (defaultTransactionManager == null) {
            defaultTransactionManager = this.beanFactory.getBean(PlatformTransactionManager.class);
            this.transactionManagerCache.putIfAbsent(
                DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
        }
    }
    return defaultTransactionManager;
}

4.3 开启事务

createTransactionIfNecessary 方法的作用是为目标方法创建一个 TransactionInfo 对象,主要分为两步。第一步,当 TransactionAttribute 和事务管理器同时存在时,尝试获取 TransactionStatus 实例。但可能为 null,是否创建新事务是由传播行为决定的,先假定调用 getTransaction 方法会创建一个新事务。第二步,调用 prepareTransactionInfo 方法,构建 TransactionInfo 对象。

protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm, TransactionAttribute txAttr, String joinpointIdentification) {
    //如果没有指定事务名称,则使用方法标识
    if (txAttr != null && txAttr.getName() == null) {
        ((DefaultTransactionAttribute)txAttr).setName(joinpointIdentification);
    }

    TransactionStatus status = null;
    if (txAttr != null && tm != null) {
        //获取事务,如有必要创建新事务(涉及到传播行为)
        status = tm.getTransaction(txAttr);
    }
    return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}

TransactionStatus 代表一个孤立的事务方法,因此需要建立与其他事务方法的联系,形成事务的传播链prepareTransactionInfo 方法主要做了两件事,首先检查方法上是否存在事务属性,如果存在则将 TransactionStatus 绑定到 TransactionInfo 上。然后将当前方法的事务信息绑定到线程上,假如线程上已经绑定了事务信息则移除,并让新的事务信息指向上一个事务信息,形成链表结构。

protected TransactionInfo prepareTransactionInfo(PlatformTransactionManager tm, TransactionAttribute txAttr,
                                                     String joinpointIdentification, TransactionStatus status) {
    TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification);
    //方法上声明了事务注解,才能设置TransactionStatus
    if (txAttr != null) {
        txInfo.newTransactionStatus(status);
    }

    //将TransactionInfo绑定到线程上
    txInfo.bindToThread();
    return txInfo;
}

我们来看 bindToThread 方法的具体实现。首先获取线程绑定的 TransactionInfo,并指向 oldTransactionInfo 字段,形成链表。oldTransactionInfo 字段代表上一个事务方法对应的事务信息,如果当前方法就是事务链的第一个方法,该字段为空。然后将当前方法的事务信息绑定到线程上,也就是说 transactionInfoHolder 始终指向正在执行的事务方法对应的事务信息,具有独占性与排他性

protected final class TransactionInfo {

    private void bindToThread(){
        this.oldTransactionInfo = transactionInfoHolder.get();  //指向前一个TransactionInfo,形成链表
        transactionInfoHolder.set(this);
    }
}

4.4 异常处理

如果目标方法调用过程中抛出异常,由外层的 try...catch 块捕获之后,交由 completeTransactionAfterThrowing 方法处理。从方法名可以看到,出现异常时需要做的是将事务完成(complete),完成意味着回滚或提交,问题在于既然已经抛出异常,为什么还可能会提交?

结合代码来看,首先确保事务存在,也就是 TransactionStatus 不能为空。接下来的两个分支是关键,分支一,如果抛出的异常是运行时异常或错误则回滚。否则说明是受检查的异常,忽略掉,进入提交流程。运行时异常和受检查的异常有什么区别?为什么受检查的异常不会被回滚?我们将在稍后专门分析。总的来说,异常处理的关键在于执行回滚还是提交流程,这一点是由异常的类型决定的

protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) {
    if(txInfo != null && txInfo.hasTransaction()){
        //抛出的异常是运行时异常或错误,则回滚
        if (txInfo.transactionAttribute.rollbackOn(ex)) {
            txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
        }
        //受检查的异常直接吞没,进入提交流程
        else{
            txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
        }
    }
}

4.5 提交与清理

提交事务的流程比较简单,如果 TransactionInfo 存在事务,也就是说 transactionStatus 属性不为空,则由事务管理器执行提交操作。同样地,事务管理器最终执行提交还是回滚,仍需根据具体情况而定。

protected void commitTransactionAfterReturning(TransactionInfo txInfo) {
    if (txInfo != null && txInfo.hasTransaction()) {
        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
    }
}

最后的收尾工作是由 cleanupTransactionInfo 方法完成的,将线程绑定的当前方法的 TransactionInfo 替换为链表中的上一个 TransactionInfo。这一操作类似于数据结构中的栈,当一个方法执行完毕,将该方法对应的事务信息从栈里弹出。

protected void cleanupTransactionInfo(TransactionInfo txInfo) {
    if (txInfo != null) {
        //与bindToThread方法的操作相反
        txInfo.restoreThreadLocalStatus();
    }
}

5. 异常处理机制

5.1 概述

对于声明式事务来说,运行时异常和受检查异常的处理逻辑是截然不同的。为了搞清楚这个问题,我们首先了解什么是运行时异常和受检查的异常。

  • 运行时异常是指 RuntimeException 及其子类,其运行结果是未知的,可以不使用 try...catch 块来处理,因此也称未检查异常(unchecked exception)。
  • 受检查的异常是指 Exception 及其子类(不包括 RuntimeException),其结果在编译期是可以预见的,因此必须被 try...catch 块处理。

综上所述,受检查的异常是已知的、可控的,业务方充分地认识到潜在的风险,因此仍然执行提交流程。运行时异常是未知的、不可控的,错误(Error)则更为严重,这两者都需要回滚。换个角度来看,Java 的底层机制确保了受检查的异常一定会被处理,Spring 相信业务方能妥善处理,总体持乐观态度,因此选择将异常吞掉(swallow),继续提交事务。对于运行时异常来说,由于其不确定性,尽管业务方可以进行事先防范,但 Spring 并不能假定业务方能做到万无一失。因此 Spring 对此持悲观态度,选择回滚事务也就可以理解了。

注:Spring 所作出的种种努力,目的是最大限度地确保程序运行的连续性,将异常所产生的副作用降到最低。

5.2 流程分析

我们知道,事务管理器的 commit 方法未必会执行提交操作。在第二个分支中,仍有可能执行回滚操作。举个例子,一个事务中有 A、B 两个事务方法,分为两种情况来讨论。第一种情况,B 方法抛出运行时异常,执行回滚流程。由于 B 方法是内层方法,仅标记 rollbackOnly 属性。等到 A 方法提交时,发现 rollbackOnly 的标记为 true,因此转而执行回滚操作。由于 A 方法是外层方法,需要回滚整个事务并抛出异常,整个业务宣告失败。

6.3 未检查的异常流程图.png

第二种情况,B 方法抛出受检查的异常,执行提交流程。由于 B 方法是内层方法,并没有执行实际的提交操作,仅完成了最后的清理工作。然后回到 A 方法的处理流程,由于 rollbackOnly 没有标记为 true,直接提交完成事务。也就是说,B 方法的业务异常被吞掉了(swallow),A 方法并没有感知到 B 方法的异常,整个业务被视为执行成功。

6.4 受检查的异常流程图.png

5.3 回滚受检查异常

Spring 事务默认忽略对受检查异常的处理,但仍提供了一种机制允许对受检查异常进行回滚。@Transactional 注解定义了 rollbackForrollbackForClassName 属性,作用是对指定类型的异常进行回滚。这两个属性由 RuleBasedTransactionAttribute 提供支持,该类重写了 rollbackOn 方法。出于简化代码的考虑,我们没有实现这个功能,感兴趣的读者可自行完成。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Transactional {
    Class<? extends Throwable>[] rollbackFor() default {};
    String[] rollbackForClassName() default {};
}

6. 测试

6.1 运行时异常

首先修改配置类 DataSourceConfig,注册 DataSourceTransactionManager 作为默认的事务管理器。

//测试类
@Configuration
@PropertySource("jdbc.properties")
@ComponentScan("tx.test.common")
public class DataSourceConfig {
    @Bean
    public DataSourceTransactionManager transactionManager(DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }
}

测试类 UserService 定义了 handleWithRuntimeException 方法,执行了一个除法计算,由于除数是 0,将抛出算术异常。而 ArithmeticExceptionRuntimeException 的子类,因此属于运行时异常。

//测试类
public class UserService {

    @Transactional
    public void handleWithRuntimeException(){
        int i = 10 / 0;
    }
}

测试代码比较简单,完成准备工作之后,获取 UserService 实例,并调用 handleWithRuntimeException 方法。

//测试方法
@Test
public void testRuntimeException(){
    //准备工作
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(AspectConfig.class);

    //注册Aop组件
    AopConfigUtils.registerAutoProxyCreatorIfNecessary(context);
    context.refresh();

    UserService service = context.getBean(UserService.class);
    service.handleWithRuntimeException();
}

从测试结果可以看到,在执行 UserServicehandleWithRuntimeException 方法前开启了事务。然后抛出异常,对事务进行了回滚。

[Tx] [事务管理] --> 事务不存在,开启新事务
[Tx] [事务管理] --> 回滚事务
java.lang.ArithmeticException: / by zero
	at tx.common.UserService.handleWithRuntimeException(UserService.java:34)

6.2 受检查异常

测试类 UserService 定义了 handleWithRuntimeException 方法,直接抛出 IO 异常。由于 IOExceptionException 的子类,属于受检查的异常,继续向上抛出异常。

//测试类
public class UserService {

    @Transactional
    public void handleWithCheckedException() throws IOException {
        throw new IOException("模拟IO异常");
    }
}

测试方法与上一个测试基本相同,唯一的区别是使用 try...catch 块包裹 handleWithCheckedException 方法,目的是处理受检查的异常。

//测试方法
@Test
public void testCheckedException(){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(TransactionConfig.class);

    //注册Aop组件
    AopConfigUtils.registerAutoProxyCreatorIfNecessary(context);
    context.refresh();

    UserService service = context.getBean(UserService.class);
    try {
        service.handleWithCheckedException();
    } catch (IOException e) {
        System.out.println("捕获受检查异常:" + e.getMessage());
    }
}

从测试结果可以看到,在执行 FooServicehandleWithCheckedException 方法前开启了事务。更为重要的是,抛出的 IOException 异常并没有影响事务的正常提交。最后对受检查异常进行捕获,得到了具体的异常信息。

[Tx] [事务管理] --> 事务不存在,开启新事务
[Tx] [事务管理] --> 提交事务
捕获受检查异常:模拟IO异常

7. 总结

本节介绍了声明式事务的拦截逻辑,总的来说,TransactionAspectSupport 做了三件事。其一,为事务方法寻找事务管理器,实际上也就指定了数据源。其二,在开启事务阶段,通过 TransactionInfo 构建了一个事务链,也就是 Spring 事务的横向结构。其三,同样在开启事务阶段,还处理了事务的传播行为,这部分内容将在后面详细讨论。

我们知道,事务有三个主要操作,即开启、回滚、提交事务。这些操作的细节已经在 DataSourceTransactionManager 类中详细介绍过了,TransactionAspectSupport 的作用是将这些操作整合起来,形成一套完整的事务管理流程。

此外,需要注意的是对异常的处理,一共有三种情况:

  • 运行时异常和错误:特点是不可预知,框架不认为用户可以妥善处理,对此持悲观态度,因而进入回滚流程。
  • 受检查的异常:也称业务异常,在编译期必须使用 try...catch 包裹。框架认为用户能够妥善处理业务异常,因此默认忽略该类异常,转而进入提交流程。
  • 特殊情况:Spring 还提供了回滚受检查异常的机制,通过 @Transactional 注解的 rollbackFor 等属性指定要回滚的异常类型。

8. 项目信息

新增修改一览,新增(0),修改(4)。

tx
└─ src
   ├─ main
   │  └─ java
   │     └─ cn.stimd.spring
   │        └─ transaction
   │           └─ interceptor
   │              └─ TransactionAspectSupport.java (*)
   └─ test
      └─ java
         └─ tx
            ├─ aop
            │  └─ TxAopTest.java (*)
            ├─ common
            │  └─ UserService.java (*)
            └─ jdbc
               └─ DataSourceConfig.java (*)

注:+号表示新增、*表示修改

注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。


欢迎关注公众号【Java编程探微】,加群一起讨论。

原创不易,觉得内容不错请关注、点赞、收藏。