本文首发微信公众号【Java编程探微】
1. 前言
如前所述,声明式事务建立在 Spring AOP 的基础之上。我们知道,由于 AOP 代理的特殊实现方式,可能会出现 AOP 失效的问题。同样地,声明式事务也可能出现事务失效的问题。本节先回顾一下 AOP 失效的原理,温故可以知新,我们还将详细分析事务失效的几种常见情况。
2. AOP 代理机制
2.1 基本原理
我们在第二章第五节详细介绍了 AOP 代理机制,这里简要地回顾一下。AOP 代理实际上是个包装类,持有一个目标对象,以及一个 Advisor 集合。对于声明式事务来说,持有的 Advisor 实例是 BeanFactoryTransactionAttributeSourceAdvisor。假设 method1 方法声明了 @Transactional 注解,那么 method1 方法在调用前会被包装成一个增强方法。
增强方法有两个特点,一是持有一个拦截器链,是从 Advisor 集合中提取出来的。对于声明式事务来说,拦截器就是 TransactionInterceptor。二是在调用目标方法之前,先执行拦截器链,也就是说 TransactionInterceptor 负责接下来的流程。这样一来,事务操作通过 AOP 代理无缝地嵌入到业务方法之中。
2.2 AOP 失效问题
当我们在一个增强方法中调用同一个类中的另一个增强方法,第二个方法的增强逻辑会失效,这就是 AOP 失效的问题。这是因为调用第一个方法时,引用指向的是代理对象。首先执行的是拦截器链,也就是增强逻辑。当进入第一个方法内部,this 引用指向的是目标对象,此时不存在拦截器链,第二个方法只是一个普通方法。
示例代码如下,调用 foo 方法时,实际是通过代理对象的引用调用的,因此会触发增强逻辑。但是在 foo 方法内部,this 引用指向的是目标对象,此时 bar 方法只是一个普通方法,不会触发增强逻辑。AOP 实质上是通过代理对象的回调来触发拦截器链,从而执行增强逻辑。因此,解决思路就是在调用 bar 方法时,确保使用的是代理对象的引用。
//示例代码:模拟AOP失效
public class Target {
@Transactional
public void foo() {
this.bar();
}
@Transactional
public void bar() {}
}
3. 事务方法
3.1 概述
前边提到,声明式事务是基于 Spring AOP 实现的,其特点是需要借助代理对象对调用的方法进行拦截,然后执行增强逻辑。换句话说,事务拦截是否生效要看调用方是代理对象的引用,还是普通对象的引用。因此,有效的事务方法必须符合两个条件:
- 事务方法所在的类至少声明了一个
@Transactional注解,因为只有声明了事务注解,所在的类才会被代理。 - 必须保证事务方法的调用方是代理对象,这样才会执行增强逻辑。
3.2 内部调用
我们先来看内部调用的情况,在调用 a 方法时会由 Foo 的代理对象进行拦截,从而执行事务的相关处理。但是在 a 方法内部,this 关键字指向的是目标对象,调用 b 方法并不会触发拦截,尽管 b 方法同样声明了事务注解。在这种情况下,我们认为 b 方法是无效的事务方法,也就是说即使在事务注解中指定了 timeout 或 propagation 等属性,都不会起作用。
//示例代码:内部调用一
public class Foo {
@Transactional
public void a() {
this.b();
}
//事务拦截失效
@Transacional
public void b() {}
}
内部调用还有一种变体,虽然形式有所区别,实际效果都是导致 b 方法的事务失效。示例代码如下,a 方法虽然没有声明事务注解,但 Foo 仍是一个代理类。因此调用 a 方法时,仍会执行事务拦截,只不过没有触发真正的事务逻辑。同样地,a 方法内部的 this 指向目标对象(非代理),b 方法只会作为普通方法调用。
//示例代码:内部调用二
public class Foo {
//执行拦截,但无效
public void a() {
this.b();
}
//不执行拦截,失效
@Transacional
public void b() {}
}
需要注意的是,两种情况还是有所区别的。第一种情况,假如 b 方法报错,由于 a 方法仍然是事务方法,整个操作还是会回滚。在第二种情况下,由于 a 方法没有执行实际的事务逻辑,一旦 b 方法报错,也不会进行回滚。即使两个方法都是成功的,由于 Connection 没有关闭自动提交,SQL 操作是分别提交的,从这一点来说,它们不能视为一个事务。
3.3 外部调用
接下来我们再来看外部调用的情况,Foo 是一个代理,Bar 是普通对象。由此可见,a 方法是一个事务方法,b 方法可以看做是 a 方法的一部分。这种情况实际上与内部调用的第一种情况差不多,尽管调用的是其他类的方法。
//示例代码:外部调用一
public class Foo {
private Bar bar;
@Transactional
public void a() {
this.bar.b();
}
}
public class Bar {
//不执行拦截,普通方法
public void b(){}
}
第二种情况与第一种情况相比,Bar 类多了一个声明 @Transactional 注解的 c 方法,变成了一个代理。当 a 方法调用 b 方法时,尽管 b 方法没有声明事务注解,但此时的 bar 参数指向代理对象,仍然会触发拦截逻辑。但是由于 b 方法没有声明事务注解,实际上不会执行事务操作。从结果上来说,这两种情况可以看做是一回事。
//示例代码:外部调用二
public class Foo {
private Bar bar;
@Transactional
public void a() {
this.bar.b();
}
}
public class Bar {
public void b(){}
@Transactional
public void c() {}
}
3.4 标准模型
以上讨论了内部调用和外部调用的众多失效情况,都是为了说明由于 Spring AOP 特性的影响,一个方法是不是有效的事务方法是由多方面的因素共同决定的。我们需要一个标准模型,以便研究事务方法之间的关系,以及事务的传播行为。标准模型是一个外部调用,每个类都是代理对象,所有方法都声明了事务注解。
- 外部调用避免了 Spring AOP 特性造成的影响,即内部调用的无效化(不考虑使用
AopContext特殊处理) - 代理对象保证了调用方法前,先执行事务拦截的增强逻辑
- 事务注解说明方法拥有实际的事务语义,而不是空事务的实现
在示例代码中,Foo 和 Bar 都是代理类,a 方法和 b 方法都声明了事务注解。当调用 a 方法时,触发了事务拦截。然后调用 b 方法,由于 bar 变量的引用指向代理对象,因此 b 方法也触发事务拦截。在这种情况下,b 方法不能看作是 a 方法的一部分,只能说两个方法属于同一个事务(在不考虑传播行为的情况下)。
//示例代码:标准模型
public class Foo {
private Bar bar;
@Transactional
public void a() {
this.bar.b();
}
}
public class Bar {
@Transactional
public void b(){}
}
综上所述,我们在今后的讨论所说的事务方法一律指有效的事务方法,除标准模型外,内部调用和外部调用的情况均视为事务方法内部的处理流程。有了标准模型,我们就可以清晰地划分,比如 a 方法是一个新事务。b 方法不是新事务,而是属于 a 方法事务的一部分。接下来我们还会讨论事务的传播行为,也是建立在标准模型之上进行分析。
注:矩形代表一个类,实线表示代理对象,虚线表示普通对象,有效的事务方法使用绿色和蓝色进行标识。
4. 导入事务组件
在第三章 context 模块中,我们介绍了导入机制。而 AOP 组件几乎是必须要导入的,因此使用 @EnableAopProxy 注解来模拟 @EnableAspectJAutoProxy 注解。声明式事务除了依赖 AOP 组件,自身也需要注册一些组件。因此,Spring 为声明式事务提供了自动导入功能。
先来看 @EnableTransactionManagement 注解,这是自动导入的入口。通过导入 ImportSelector 实现类方式来导入组件。
proxyTargetClass属性:表示是否代理目标类,默认 false,说明使用 JDK 动态代理。如果为 true,则表示使用 CGLIB 代理。mode属性:表示通知模式,PROXY表示基于 JDK 动态代理,可选ASPECTJ。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {
boolean proxyTargetClass() default false;
AdviceMode mode() default AdviceMode.PROXY;
}
TransactionManagementConfigurationSelector 的具体实现如下:获取注解的 mode 属性,如果指定为 proxy,表示 JDK 动态代理,导入两个组件。AutoProxyRegistrar 导入了 AOP 相关的组件,之前已介绍,不赘述。ProxyTransactionManagementConfiguration 负责导入事务相关的组件。
public class TransactionManagementConfigurationSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(
importingClassMetadata, EnableTransactionManagement.class);
AdviceMode adviceMode = attributes.getEnum("mode");
switch (adviceMode) {
/**
* 如果是JDK动态代理模式,需要做两件事情
* 1. 注册AOP的基础组件类InfrastructureAdvisorAutoProxyCreator
* 2. 注册事务相关的组件类
*/
case PROXY:
return new String[] {AutoProxyRegistrar.class.getName(),
ProxyTransactionManagementConfiguration.class.getName()};
//ASPECTJ 略
default:
throw new IllegalArgumentException("解析@EnableTransactionManagement失败,未知的AdviceMode: " + adviceMode);
}
}
}
ProxyTransactionManagementConfiguration 是一个配置类,导入了三个事务相关的组件。这样一来,我们只需要声明一个注解,就可以导入 AOP 和事务的相关组件,简化了开发流程。
@Configuration
public class ProxyTransactionManagementConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TransactionAttributeSource transactionAttributeSource() {
return new AnnotationTransactionAttributeSource();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public TransactionInterceptor transactionInterceptor(TransactionAttributeSource transactionAttributeSource) {
TransactionInterceptor interceptor = new TransactionInterceptor();
interceptor.setTransactionAttributeSource(transactionAttributeSource);
return interceptor;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor(
TransactionAttributeSource transactionAttributeSource, TransactionInterceptor transactionInterceptor) {
BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
advisor.setTransactionAttributeSource(transactionAttributeSource);
advisor.setAdvice(transactionInterceptor);
return advisor;
}
}
5. 测试
本测试有三个目的,一是事务方法的标准模型,二是事务失效的情况,三是自动导入功能。先来看 AnnotationTxAopConfig 配置类,这里使用@EnableTransactionManagement 注解,不再需要手动注册事务组件。
//测试类
@Configuration
@Import(DataSourceConfig.class)
@EnableTransactionManagement
public class AnnotationTxAopConfig {}
接下来是两个测试类。在 FooService 的 step1 方法中,先调用了本类的 step2 方法,但事务并没有起效,因为这是一个内部调用。然后调用 BarService 的 step3 方法,这是标准模型,因此事务会生效。
//测试类
public class FooService {
@Autowired
private BarService barService;
@Transactional
public void step1(){
System.out.println("执行step1...");
//内部调用,无效事务
this.step2();
//标准模型,有效事务
this.barService.step3();
}
@Transactional
public void step2(){
System.out.println("执行step2...");
}
}
public class BarService {
@Transactional
public void step3() {
System.out.println("执行step3...");
}
}
测试方法需要注意两点,一是注册了 MyTransactionInterceptor 组件,我们只想观察拦截器是否执行,不需要执行实际的数据库操作。二是没有手动注册 AOP 组件,已经由 @EnableTransactionManagement 注解代劳了。
//测试方法
@Test
public void testTransactionInvocation(){
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBeanDefinition("transactionInterceptor", new RootBeanDefinition(MyTransactionInterceptor.class));
context.register(TxAopConfig.class);
context.register(FooService.class);
context.register(BarService.class);
context.refresh();
FooService fooService = context.getBean("fooService", FooService.class);
fooService.step1();
}
从测试结果可以看到,在执行 step1 方法前进行拦截,这是有效事务。而 step2 方法并没有进行拦截,说明内部调用被当作普通方法处理。step3 方法是标准模型,因此也打印了拦截日志。由此可见,测试结果是符合预期的。
调用事务 --> 目标类: tx.aop.FooService, 方法: step1
执行step1...
执行step2...
调用事务 --> 目标类: tx.aop.BarService, 方法: step3
执行step3...
6. 总结
本节是声明式事务的延伸内容。由于声明式事务是基于 Spring AOP 实现的,因此必然受到 AOP 失效的影响。我们详细分析了事务失效的几种情况,包括内部调用和外部调用,并规定了事务方法的标准模型。标准模型是一个外部调用,每个类都是代理对象,所有方法都声明了事务注解。标准模型有助于我们研究事务方法之间的关系,比如事务的传播行为(下节介绍)。
此外,在实际开发中几乎都要用到事务,因此 Spring 框架提供了便捷的引入方式。只需要声明 @EnableTransactionManagement 注解,就会自动导入事务相关的组件。不仅如此,由于事务是基于 AOP 实现的,因此也会导入 AOP 相关的组件。
7. 项目信息
新增修改一览,新增(6),修改(1)。
tx
└─ src
├─ main
│ └─ java
│ └─ cn.stimd.spring.transaction
│ └─ annotation
│ ├─ EnableTransactionManagement.java (+)
│ ├─ ProxyTransactionManagementConfiguration.java (+)
│ └─ TransactionManagementConfigurationSelector.java (+)
└─ test
└─ java
└─ tx
└─ aop
├─ AnnotationTxAopConfig.java (+)
├─ BarService.java (+)
├─ FooService.java (+)
└─ TxAopTest.java (*)
注:+号表示新增、*表示修改
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
欢迎关注公众号【Java编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。