本文首发微信公众号【Java编程探微】
1. 前言
上一节介绍了如何通过 Spring AOP 来构建事务切面,具体来说需要定义三个组件,即切点、拦截器和 Advisor,然后声明 @Transactional 注解就可以标记一个事务方法。当调用事务方法时,代理对象通过 TransactionInterceptor 来执行事务相关操作。这样做的好处是将业务逻辑和事务操作解耦,用户只需要关心具体业务的实现即可。那么,事务拦截器是如何发挥作用的,本节将进行详细的讨论。
2. 事务信息
2.1 TransactionInfo
TransactionInfo 是 TransactionAspectSupport 的内部类,对于每个事务方法来说,都会生成对应的 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:事务信息,完整地描述一个事务方法。最关键的是通过链表结构,将所有的事务方法串联起来,构成了一个事务链。
综上所述,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实例,TransactionAspectSupport的txInfoHolder字段指向 A 方法的事务信息。 - 调用 B 方法,创建
TransactionInfo实例,并指向 A 方法对应的TransactionInfo。TransactionAspectSupport的txInfoHolder字段转而指向 B 方法的事务信息。
以此类推,当调用 C 方法时,重复 B 方法的操作流程即可。当 C 方法执行完毕,C 方法对应的 TransactionInfo 会从事务链中移除,然后继续执行 B 方法的剩余逻辑。直到 A 方法执行完毕,整条事务链都不复存在,此时所有的事务都得到了执行。
4. 拦截方法详解
4.1 主流程
invokeWithinTransaction 方法定义了事务拦截的主体流程,从全局来看,这是一个典型的环绕通知。开启事务是前置通知,回滚事务是异常通知,清除事务信息以及提交事务是返回通知。先来看准备工作,首先尝试获取方法上的事务属性,如果不存在,说明方法上没有声明 @Transactional 注解,也就不会当做事务方法处理。然后获取方法标识,形式为全类名 + 方法名。接下来的操作分为四步:
- 获取事务管理器,不同的事务管理器可能指向不同的数据源,这对于 SQL 操作至关重要。
- 获取
TransactionInfo对象,与当前方法一一对应,如有必要则尝试开启事务。 - 如果业务方法抛出异常,则进入异常处理流程。
- 如果业务方法正常执行,进入提交事务流程。
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 方法的作用是尝试获取事务管理器,涉及两方面的操作,一是缓存的处理,二是从多个事务管理器中进行选择。具体来说,可以分为三步:
-
如果事务属性为空,返回手动设置的事务管理器。一般来说,不会直接设置事务管理器,因此返回的事务管理器为 null。一旦事务对象不存在,后续事务相关的操作都不会执行,也就是说仅作为普通方法执行。
-
如果事务属性的
qualifier字段不为空,则从 Spring 容器中获取指定名称的事务管理器。 -
先尝试获取手动设置的事务管理器,如果不存在则获取默认的事务管理器。此时仍可能存在多个事务管理器,默认的是指主要的(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 方法是外层方法,需要回滚整个事务并抛出异常,整个业务宣告失败。
第二种情况,B 方法抛出受检查的异常,执行提交流程。由于 B 方法是内层方法,并没有执行实际的提交操作,仅完成了最后的清理工作。然后回到 A 方法的处理流程,由于 rollbackOnly 没有标记为 true,直接提交完成事务。也就是说,B 方法的业务异常被吞掉了(swallow),A 方法并没有感知到 B 方法的异常,整个业务被视为执行成功。
5.3 回滚受检查异常
Spring 事务默认忽略对受检查异常的处理,但仍提供了一种机制允许对受检查异常进行回滚。@Transactional 注解定义了 rollbackFor 和 rollbackForClassName 属性,作用是对指定类型的异常进行回滚。这两个属性由 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,将抛出算术异常。而 ArithmeticException 是 RuntimeException 的子类,因此属于运行时异常。
//测试类
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();
}
从测试结果可以看到,在执行 UserService的 handleWithRuntimeException 方法前开启了事务。然后抛出异常,对事务进行了回滚。
[Tx] [事务管理] --> 事务不存在,开启新事务
[Tx] [事务管理] --> 回滚事务
java.lang.ArithmeticException: / by zero
at tx.common.UserService.handleWithRuntimeException(UserService.java:34)
6.2 受检查异常
测试类 UserService 定义了 handleWithRuntimeException 方法,直接抛出 IO 异常。由于 IOException 是 Exception 的子类,属于受检查的异常,继续向上抛出异常。
//测试类
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());
}
}
从测试结果可以看到,在执行 FooService的 handleWithCheckedException 方法前开启了事务。更为重要的是,抛出的 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编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。