【重写SpringFramework】声明式事务上:构建事务切面(chapter 4-5)

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

1. 前言

上一节介绍了编程式事务,我们发现,事务操作有两个特点。一是事务操作广泛地出现在应用程序中,二是事务操作的逻辑是有迹可循的,无非是对开启事务、回滚和提交这几个操作的组合使用。换句话说,事务操作是一种公共行为,可以从众多的业务代码中剥离出来,这是很典型的面向切面编程。因此,Spring 依托已有的 AOP 功能,提供了一套使用注解进行事务管理的解决方案,即声明式事务。

声明式事务可以看做是面向切面编程的一个用例,在实际开发中得到了广泛地使用。鉴于此,我们需要先构建一个事务切面,然后才能进一步研究声明式事务是如何工作的。

2. @Transactional 注解

按照惯例,声明式事务也是以注解为切入点。@Transactional 注解的作用是将一个方法标记为事务方法,value 属性很重要,表示事务方法所属的事务管理器。我们知道一个事务管理器对应一个数据源,因此指定 value 属性相当于决定了 SQL 操作指向的数据源。

java 复制代码
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Transactional {
    //指定事务管理器,默认为空串,使用默认的事务管理器
    String value() default "";
    //事务的传播属性,默认为required
    Propagation propagation() default Propagation.REQUIRED;
    //超时时间
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    //是否为只读事务
    boolean readOnly() default false;
}

3. AOP 组件

3.1 概述

Spring 没有采用 AspectJ 框架来构成事务切面,而是使用 Spring 自带的 AOP 实现。我们知道,实现 Spring AOP 需要三大组件,Advisor、Pointcut 以及 Interceptor。声明式事务也是建立在 AOP 组件基础之上的,简单介绍如下:

  • TransactionAttributeSourcePointcut:寻找一系列符合条件的方法,构建切面
  • TransactionInterceptor:事务语义的逻辑所在,执行目标方法时进行拦截
  • BeanFactoryTransactionAttributeSourceAdvisor:将切面与增强逻辑组合起来

3.2 Advisor 组件

BeanFactoryTransactionAttributeSourceAdvisor 持有一个 Pointcut 实例,这里创建了一个匿名的 TransactionAttributeSourcePointcut 对象。也就是说,切点是固定的,只需要指定一个 TransactionAttributeSource 组件来完成构建切面的工作。

java 复制代码
public class BeanFactoryTransactionAttributeSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {
    private TransactionAttributeSource transactionAttributeSource;

    private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut(){
        @Override
        protected TransactionAttributeSource getTransactionAttributeSource() {
            return transactionAttributeSource;
        }
    };
}

3.3 Pointcut 组件

TransactionAttributeSourcePointcut 是一个抽象类,由 BeanFactoryTransactionAttributeSourceAdvisor 创建了一个匿名的实现类。该类继承了 StaticMethodMatcherPointcut,说明是对方法的静态编译信息进行匹配 。在 matches 方法中,首先获取 TransactionAttributeSource 对象,然后调用 getTransactionAttribute 方法检查指定方法或类上是否存在事务信息。如果存在,则说明目标方法需要增强。

java 复制代码
abstract class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointcut {
    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        //过滤掉TransactionalProxy、PlatformTransactionManager、PersistenceExceptionTranslator
        TransactionAttributeSource tas = getTransactionAttributeSource();
        return tas == null || tas.getTransactionAttribute(method, targetClass) != null;
    }

    protected abstract TransactionAttributeSource getTransactionAttributeSource();
}

3.4 Interceptor 组件

TransactionInterceptor 继承了 TransactionAspectSupport,事务代理对象创建完成之后,每次调用目标方法时,由 TransactionInterceptor 进行拦截。invoke 方法的逻辑并不复杂,invokeWithinTransaction 方法由父类实现,在适当的时机回调 InvocationCallback 匿名对象,然后执行目标方法。

注:本节暂不涉及 invokeWithinTransaction 方法的具体逻辑,事务拦截的具体逻辑将在下节介绍,因此先略过父类 TransactionAspectSupport

java 复制代码
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Class<?> targetClass = invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null;
        return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
            @Override
            public Object proceedWithInvocation() throws Throwable {
                //回调执行目标方法
                return invocation.proceed();
            }
        });
    }
}

4. 事务切点

4.1 继承结构

前面介绍了事务切面的三个组件,Advisor 的作用是整合 Pointcut 和 Interceptor,这一点很好理解。Interceptor 的作用是执行事务操作,下节详细介绍。本节的重点是 Pointcut,也就是说,我们要知道一个对象是否需要被代理,或者说如何判断一个方法是事务方法。

事务切点的具体工作是交由 TransactionAttributeSource 处理的。继承结构看似复杂,但分工很明确,可以分为三组:

  • 事务信息:包括 TransactionAttribute 接口及其子类,作用是描述声明式事务的相关信息
  • 事务注解解析器:包括 TransactionAnnotationParser 接口及其子类,负责对事务相关的注解进行解析
  • 事务信息的来源:包括 TransactionAttributeSource 接口及其子类,负责检索声明式事务的相关信息

4.2 TransactionAttribute

TransactionAttribute 扩展了 TransactionDefinition 接口,作用是定义了声明式事务的行为。

  • getQualifier 方法:为事务方法选择一个事务管理器,如果为空串则使用默认的事务管理器
  • rollbackOn 方法:是否在指定异常上进行回滚操作
java 复制代码
public interface TransactionAttribute extends TransactionDefinition {
    String getQualifier();
    boolean rollbackOn(Throwable ex);
}

DefaultTransactionAttribute 不仅是 TransactionAttribute 接口的实现类,还扩展了 DefaultTransactionDefinition,作用是为 @Transactional 注解提供支持。该类实现了 rollbackOn 方法,对运行时异常和错误进行回滚。问题在于 Exception 有多种实现,为何仅回滚运行时异常?下一节将会详细讨论。

java 复制代码
public class DefaultTransactionAttribute extends DefaultTransactionDefinition implements TransactionAttribute{
    private String qualifier;
    private String descriptor

    @Override
    public boolean rollbackOn(Throwable ex) {
        return ex instanceof RuntimeException || ex instanceof Error;
    }
}

4.3 TransactionAttributeSource

TransactionAttributeSource 接口的作用是检索事务信息,getTransactionAttribute 方法用于查找指定方法上的事务属性,如果返回 null 说明是一个普通方法,否则认为是一个事务方法。

java 复制代码
public interface TransactionAttributeSource {
    TransactionAttribute getTransactionAttribute(Method method, Class<?> targetClass);
}

AbstractFallbackTransactionAttributeSourceTransactionAttributeSource 接口的抽象子类,完成了查找事务信息的主要逻辑。该类有两个特点,首先,由于每次执行拦截逻辑都需要拿到目标方法的事务信息,因此有必要将 TransactionAttribute 缓存起来。其次,在查找事务信息的过程中存在三次降级的过程,具体的逻辑在构建事务切面时详细介绍。

java 复制代码
public abstract class AbstractFallbackTransactionAttributeSource implements TransactionAttributeSource{
    private final Map<Object, TransactionAttribute> attributeCache = new ConcurrentHashMap<>(1024);
    private static final TransactionAttribute NULL_TRANSACTION_ATTRIBUTE = new DefaultTransactionAttribute();
}

AnnotationTransactionAttributeSource 是主要的实现类,从类名中可以看出,该类是通过声明注解的方式提供事务信息的来源。该类持有一个 TransactionAnnotationParser 集合,并在构造函数中添加了一个默认的事务注解解析器,实际的解析工作是由 SpringTransactionAnnotationParser 完成的。

java 复制代码
public class AnnotationTransactionAttributeSource extends AbstractFallbackTransactionAttributeSource {
    private final Set<TransactionAnnotationParser> annotationParsers;

    public AnnotationTransactionAttributeSource() {
        this.annotationParsers = Collections.singleton(new SpringTransactionAnnotationParser());
    }
}

4.4 TransactionAnnotationParser

TransactionAnnotationParser 是解析事务注解的策略接口,该接口有三个主要实现类,用于处理不同的情况,如下所示:

  • SpringTransactionAnnotationParser:解析 @Transactional 注解,基于 JDBC 事务
  • Ejb3TransactionAnnotationParser:解析 @TransactionAttribute 注解,基于 EJB3 的事务(仅了解)
  • JtaTransactionAnnotationParser:解析 @javax.transaction.Transactional 注解,基于 JTA 分布式事务(仅了解)
java 复制代码
public interface TransactionAnnotationParser {
    TransactionAttribute parseTransactionAnnotation(AnnotatedElement element);
}

5. 构建事务切面

5.1 概述

我们知道,只要一个对象至少存在一个增强方法,那么就应该被代理。TransactionAttributeSourcePointcutmatches 方法负责检查指定方法是否满足要求。实际的逻辑是由 TransactionAttributeSource 完成的,接下来我们以 getTransactionAttribute 方法为切入点来分析具体的匹配逻辑。

java 复制代码
abstract class TransactionAttributeSourcePointcut extends StaticMethodMatcherPointcut {

    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        TransactionAttributeSource tas = getTransactionAttributeSource();
        //获取方法的事务属性,如果不为 null 则表示匹配
        return tas == null || tas.getTransactionAttribute(method, targetClass) != null;
    }
}

5.2 代码实现

接下来的操作由三个类完成,因此可以分为三步。第一步,AbstractFallbackTransactionAttributeSource 构建了检索事务信息的基本流程。在 getTransactionAttribute 方法中,先尝试从缓存中获取指定方法的事务信息,然后调用 computeTransactionAttribute 方法,将注解上的信息转译出来。如果返回值不为空,则说明这是一个增强方法。

java 复制代码
public abstract class AbstractFallbackTransactionAttributeSource implements TransactionAttributeSource {

    @Override
    public TransactionAttribute getTransactionAttribute(Method method, Class<?> targetClass) {
        Object cacheKey = new MethodClassKey(method, targetClass);
        TransactionAttribute cached = this.attributeCache.get(cacheKey);
        if (cached != null) {
            //如果等于默认空值说明无事务,返回null
            return cached != NULL_TRANSACTION_ATTRIBUTE ? cached : null;
        }

        //寻找事务信息
        TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass);
        if (txAttr == null) {
            this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE);
        }else{
            this.attributeCache.put(cacheKey, txAttr);
        }
        return txAttr;
    }
}

computeTransactionAttribute 方法执行过程中,优先查找方法上的注解,其次才是类上的注解。如果采用 JDK 动态代理的方式,还需要检查接口方法以及接口上的注解。这样一共会出现四种情况,根据优先级从高到低逐步降级执行,这也是类名中有 fallback 一词的原因所在。

注:Spring 早期版本默认使用 JDK 动态代理,现在则换成了 CGLIB 代理。再者,我们通常将注解声明在方法上,而非类或接口上,这样处理起来更灵活。绝大多数情况下,我们只关心第一种情况,即在实现类的方法上声明注解。

java 复制代码
public abstract class AbstractFallbackTransactionAttributeSource implements TransactionAttributeSource {

    protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
        //如果是Cglib代理,忽略自动生成的的子类,获取原始类
        Class<?> userClass = ClassUtils.getUserClass(targetClass);
        //如果是JDK代理,method可能是接口定义的方法,获取实现类上的方法
        Method specificMethod = ClassUtils.getMostSpecificMethod(method, userClass);
        //如果存在泛型参数,忽略桥接方法,找到原始的方法
        specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);

        //1. 获取目标类的方法上的事务属性
        TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
        if (txAttr != null) {
            return txAttr;
        }

        //2. 获取目标类上的事务属性(第一次降级)
        txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
        if (txAttr != null) {
            return txAttr;
        }

        //如果使用JDK代理的方式创建对象,尝试获取接口上的注解
        if(method != specificMethod){
            //3. 获取接口方法上的事务属性(第二次降级)
            txAttr = findTransactionAttribute(method);
            if (txAttr != null) {
                return txAttr;
            }

            //4. 获取接口上的事务属性(第三次降级)
            txAttr = findTransactionAttribute(method.getDeclaringClass());
            if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
                return txAttr;
            }
        }
        return null;
    }
}

第二步,子类 AnnotationTransactionAttributeSource 重写了两个模板方法 findTransactionAttribute,作用是寻找指定类(包括接口)或方法上的事务信息。这两个重载方法最终都调用了 determineTransactionAttribute 方法,实际委托给 SpringTransactionAnnotationParser 来处理。

java 复制代码
public class AnnotationTransactionAttributeSource extends AbstractFallbackTransactionAttributeSource {
    //寻找方法上的注解
    @Override
    protected TransactionAttribute findTransactionAttribute(Method method) {
        return determineTransactionAttribute(method);
    }

    //寻找类上的注解
    @Override
    protected TransactionAttribute findTransactionAttribute(Class<?> clazz) {
        return determineTransactionAttribute(clazz);
    }

    protected TransactionAttribute determineTransactionAttribute(AnnotatedElement element){
        for (TransactionAnnotationParser parser : this.annotationParsers) {
            TransactionAttribute txAttr = parser.parseTransactionAnnotation(element);
            if (txAttr != null) {
                return txAttr;
            }
        }
        return null;
    }
}

第三步,SpringTransactionAnnotationParser 负责解析事务注解。parseTransactionAnnotation 方法的实现比较简单,分为两步。首先获取指定类或方法上的 @Transactional 注解,然后创建 DefaultTransactionAttribute 对象,逐一提取事务注解上的属性。最终返回 TransactionAttribute 对象。

java 复制代码
public class SpringTransactionAnnotationParser implements TransactionAnnotationParser{
    //解析事务注解
    @Override
    public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) {
        //寻找方法上的@Transactional
        AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(element,Transactional.class, false, false);
        if (attributes != null) {
            return parseTransactionAnnotation(attributes);
        }
        return null;
    }

    //解析@Transactional等注解
    private TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) {
        DefaultTransactionAttribute txAttr = new DefaultTransactionAttribute();
        Propagation propagation = attributes.getEnum("propagation");
        txAttr.setPropagationBehavior(propagation.value());
        txAttr.setTimeout(attributes.getNumber("timeout").intValue());
        txAttr.setReadOnly(attributes.getBoolean("readOnly"));
        txAttr.setQualifier(attributes.getString("value"));

        //rollback相关属性,略
        return txAttr;
    }
}

6. 测试

准备一个配置类 AspectConfig,通过单例方法向 Spring 容器注册事务切面的相关组件,注意这些方法的顺序是固定的。

  • 注册 AnnotationTransactionAttributeSource,该组件是 TransactionAttributeSourcePointcut 的一部分,负责对方法进行匹配。
  • 注册 TransactionInterceptor ,实现事务拦截逻辑。
  • 注册 BeanFactoryTransactionAttributeSourceAdvisor,持有一个切点和拦截器。
java 复制代码
//测试类
@Configuration
@Import(DataSourceConfig.class)
public class AspectConfig {

    @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;
    }
}

测试类 MyTransactionInterceptor 用于模拟事务拦截,仅打印日志。为了不影响 TransactionInterceptor 的正常流程,所以使用子类来模拟。

java 复制代码
//测试类
public class MyTransactionInterceptor extends TransactionInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Class<?> targetClass = AopUtils.getTargetClass(invocation.getThis());
        System.out.println("调用事务 --> 目标类: " + targetClass.getName() + ", 方法: " + invocation.getMethod().getName());
        return invocation.proceed();
    }
}

修改业务类 UserService,定义一个方法并声明 @Transactional 注解。由于尚未实现事务的真正逻辑,仅打印日志表示方法执行了。

java 复制代码
//测试类
@Service
public class UserService {

    @Transactional
    public void save(){
        System.out.println("UserService save...");
    }
}

测试方法分为四步。第一步,创建 Spring 容器,引入 AspectConfig 配置类,注册事务切面的三个组件。第二步,注册 MyTransactionInterceptor,覆盖掉默认的事务拦截器,便于测试。第三步,由于事务是基于 AOP 功能运行的,因此还要注册 AOP 相关的组件。第四步,刷新容器,获取 UserService 对象,然后调用事务方法。

java 复制代码
//测试方法
@Test
public void testTransactionAspect(){
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(AspectConfig.class);
    //注册MyTransactionInterceptor,覆盖默认的TransactionInterceptor
    context.registerBeanDefinition("transactionInterceptor", new RootBeanDefinition(MyTransactionInterceptor.class));
    
    //注册Aop组件
    AopConfigUtils.registerAutoProxyCreatorIfNecessary(context);
    context.refresh();

    UserService userService = context.getBean("userService", UserService.class);
    userService.save();
}

从测试结果可以看到,MyTransactionInterceptor 先进行了拦截,然后打印目标方法中的日志,这说明事务 AOP 构建成功。

lua 复制代码
调用事务 --> 目标类: tx.common.UserService, 方法: save
UserService save...

7. 总结

本节介绍了构建事务切面的三大组件,即 Advisor、Interceptor 和 Pointcut。其中 Advisor 是一个复合类,持有一个拦截器和一个切点。Interceptor 则涉及具体的事务操作,暂不介绍。因此本节的重点是 Pointcut 是如何起作用的,即判断一个类是否应该被代理,或者说一个方法是否被认定为事务方法。

具体来说,TransactionAttributeSourcePointcut 将检索事务注解信息的工作交给 TransactionAttributeSource 组件完成,TransactionAttributeSource 通过降级的方式依次对目标方法、类、接口方法、接口进行检查,如果声明了 @Transactional 注解,则说明这是一个(或一组)事务方法。而事务方法是通过代理对象来实现增强逻辑的,因此声明式事务是 AOP 的一个用例。

8. 项目信息

新增修改一览,新增(16),修改(1)。

scss 复制代码
tx
└─ src
   ├─ main
   │  └─ java
   │     └─ cn.stimd.spring.transaction
   │        ├─ annotation
   │        │  ├─ AnnotationTransactionAttributeSource.java (+)
   │        │  ├─ SpringTransactionAnnotationParser.java (+)
   │        │  ├─ Propagation.java (+)
   │        │  ├─ Transactional.java (+)
   │        │  └─ TransactionAnnotationParser.java (+)
   │        └─ interceptor
   │           ├─ AbstractFallbackTransactionAttributeSource.java (+)
   │           ├─ BeanFactoryTransactionAttributeSourceAdvisor.java (+)
   │           ├─ DefaultTransactionAttribute.java (+)
   │           ├─ TransactionAspectSupport.java (+)
   │           ├─ TransactionAttribute.java (+)
   │           ├─ TransactionAttributeSource.java (+)
   │           ├─ TransactionAttributeSourcePointcut.java (+)
   │           └─ TransactionInterceptor.java (+)
   └─ test
      └─ java
         └─ tx
            ├─ aop
            │  ├─ AspectConfig.java (+)
            │  ├─ MyTransactionInterceptor.java (+)
            │  └─ TxAopTest.java (+)
            └─ common
               └─ UserService.java (*)

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

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


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

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

相关推荐
程序员小陈在成都5 分钟前
Spring Ioc源码引入:什么是IoC,IoC解决了什么问题
spring
代码不行的搬运工5 分钟前
HTML快速入门-4:HTML <meta> 标签属性详解
java·前端·html
陈随易37 分钟前
长跑8年,Node.js框架Koa v3.0终发布
前端·后端·程序员
lovebugs39 分钟前
Redis的高性能奥秘:深入解析IO多路复用与单线程事件驱动模型
redis·后端·面试
bug菌43 分钟前
面十年开发候选人被反问:当类被标注为@Service后,会有什么好处?我...🫨
spring boot·后端·spring
田园Coder1 小时前
Spring之IoC控制反转
后端
mask哥1 小时前
详解最新链路追踪skywalking框架介绍、架构、环境本地部署&配置、整合微服务springcloudalibaba 、日志收集、自定义链路追踪、告警等
java·spring cloud·架构·gateway·springboot·skywalking·链路追踪
XU磊2601 小时前
javaWeb开发---前后端开发全景图解(基础梳理 + 技术体系)
java·idea
学也不会1 小时前
雪花算法
java·数据库·oracle
晓华-warm1 小时前
国产免费工作流引擎star 5.9k,Warm-Flow版本升级1.7.0(新增大量好用功能)
java·中间件·流程图·开源软件·flowable·工作流·activities