【重写SpringFramework】事务代理对象(chapter 4-7)

113 阅读11分钟

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

1. 前言

如前所述,声明式事务建立在 Spring AOP 的基础之上。我们知道,由于 AOP 代理的特殊实现方式,可能会出现 AOP 失效的问题。同样地,声明式事务也可能出现事务失效的问题。本节先回顾一下 AOP 失效的原理,温故可以知新,我们还将详细分析事务失效的几种常见情况。

2. AOP 代理机制

2.1 基本原理

我们在第二章第五节详细介绍了 AOP 代理机制,这里简要地回顾一下。AOP 代理实际上是个包装类,持有一个目标对象,以及一个 Advisor 集合。对于声明式事务来说,持有的 Advisor 实例是 BeanFactoryTransactionAttributeSourceAdvisor。假设 method1 方法声明了 @Transactional 注解,那么 method1 方法在调用前会被包装成一个增强方法。

7.1 aop代理原理图.png

增强方法有两个特点,一是持有一个拦截器链,是从 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 方法是无效的事务方法,也就是说即使在事务注解中指定了 timeoutpropagation 等属性,都不会起作用。

//示例代码:内部调用一
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 特殊处理)
  • 代理对象保证了调用方法前,先执行事务拦截的增强逻辑
  • 事务注解说明方法拥有实际的事务语义,而不是空事务的实现

在示例代码中,FooBar 都是代理类,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 方法事务的一部分。接下来我们还会讨论事务的传播行为,也是建立在标准模型之上进行分析。

7.2 事务方法标准模型.png

注:矩形代表一个类,实线表示代理对象,虚线表示普通对象,有效的事务方法使用绿色和蓝色进行标识。

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 {}

接下来是两个测试类。在 FooServicestep1 方法中,先调用了本类的 step2 方法,但事务并没有起效,因为这是一个内部调用。然后调用 BarServicestep3 方法,这是标准模型,因此事务会生效。

//测试类
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编程探微】,加群一起讨论。

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