Spring事务用法示例与实现原理

       关于事务,简单来说,就是为了保证数据完整性而存在的一种工具,其主要有四大特性:原子性,一致性,隔离性和持久性。对于Spring事务,其最终还是在数据库层面实现的,而Spring只是以一种比较优雅的方式对其进行封装支持。本文首先会通过一个简单的示例来讲解Spring事务是如何使用的,然后会讲解Spring是如何解析xml中的标签,并对事务进行支持的。

1. 使用示例

       关于事务最简单的示例,就是其一致性,比如在整个事务执行过程中,如果任何一个位置报错了,那么都会导致事务回滚,回滚之后数据的状态将和事务执行之前完全一致。这里我们以用户数据为例,在插入用户数据的时候,如果程序报错了,那么插入的动作就会回滚。如下是用户的实体:

1public class User { 2 private long id; 3 private String name; 4 private int age; 5 6 // getter, setter... 7}

       如下是模拟插入用户数据的业务代码:

1public interface UserService { 2 void insert(User user); 3} 4 5@Service 6@Transactional 7public class UserServiceImpl implements UserService { 8 @Autowired 9 private JdbcTemplate jdbcTemplate; 10 11 @Override 12 public void insert(User user) { 13 jdbcTemplate.update("insert into user (name, age) value (?, ?)", 14 user.getName(), user.getAge()); 15 } 16}

       在进行事务支持时,Spring只需要使用者在需要事务支持的bean上使用@Transactional注解即可,如果需要修改事务的隔离级别和传播特性的属性,则使用该注解中的属性进行指定。这里默认的隔离级别与各个数据库一致,比如MySQL是Repeatable Read,而传播特性默认则为Propagation.REQUIRED,即只需要当前操作具有事务即可。如下是xml文件的配置:

1<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> 2 <property name="url" value="jdbc:mysql://localhost/test?useUnicode=true"/> 3 <property name="driverClassName" value="com.mysql.jdbc.Driver"/> 4 <property name="username" value="****"/> 5 <property name="password" value="******"/> 6</bean> 7 8<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> 9 <property name="dataSource" ref="dataSource"/> 10</bean> 11 12<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> 13 <property name="dataSource" ref="dataSource"/> 14</bean> 15 16<context:component-scan base-package="com.transaction"/> 17<tx:annotation-driven/>

       上述数据库配置用户按照各自的设置进行配置即可。可以看到,这里对于数据库的配置,主要包括四个方面:

  • DataSource配置:设置当前应用所需要连接的数据库,包括链接,用户名,密码等;
  • JdbcTemplate声明:封装了客户端调用数据库的方式,用户可以使用其他的方式,比如JpaRepository,Mybatis等等;
  • TransactionManager配置:指定了事务的管理方式,这里使用的是DataSourceTransactionManager,对于不同的链接方式,也可以进行不同的配置,比如对于JpaRepository使用JpaTransactionManager,对于Hibernate,使用HibernateTransactionManager;
  • tx:annotation-driven:主要用于事务驱动,其会通过AOP的方式声明一个为事务支持的Advisor,通过该Advisor和事务的相关配置进行事务相关操作。

       按照上述配置,我们的事务功能即配置完成,如下是我们的驱动类程序:

1public class TransactionApp { 2 @Test 3 public void testTransaction() { 4 ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml"); 5 UserService userService = ac.getBean(UserService.class); 6 User user = getUser(); 7 userService.insert(user); 8 } 9 10 private User getUser() { 11 User user = new User(); 12 user.setName("Mary"); 13 user.setAge(27); 14 return user; 15 } 16}

       运行上述程序之后,可以看到数据库中成功新增了一条数据。这里如果我们将业务代码的插入语句之后手动抛出一个异常,那么,理论上插入语句是会回滚的。如下是修改后的service代码:

1@Service 2@Transactional 3public class UserServiceImpl implements UserService { 4 @Autowired 5 private JdbcTemplate jdbcTemplate; 6 7 @Override 8 public void insert(User user) { 9 jdbcTemplate.update("insert into user (name, age) value (?, ?)", 10 user.getName(), user.getAge()); 11 throw new RuntimeException(); 12 } 13}

       这里我们手动抛出了一个RuntimeException,再次运行上述程序之后我们发现数据库中是没有新增的数据的,这说明我们的事务在程序出错后是能够保证数据一致性的。

2. 标签解析

       关于事务的实现原理,我们首先讲解Spring是如何解析标签,并且封装相关bean的,后面我们会深入讲解Spring是如何封装数据库事务的。

       根据上面的示例,我们发现,其主要有三个部分:DataSource,TransactionManager和tx:annotation-driven标签。这里前面两个部分主要是声明了两个bean,分别用于数据库连接的管理和事务的管理,而tx:annotation-driven才是Spring事务的驱动。根据本人前面对Spring自定义标签的讲解(Spring自定义标签解析与实现),可以知道,这里tx:annotation-driven是一个自定义标签,我们根据其命名空间(www.springframework.org/schema/tx)在全局范围内搜索,可以找到其处理器指定文件spring.handlers,该文件内容如下:

http\://www.springframework.org/schema/tx=org.springframework.transaction.config.TxNamespaceHandler

       这里也就是说tx:annotation-driven标签的解析在TxNamespaceHandler中,我们继续打开该文件可以看到起init()方法如下:

1@Override 2public void init() { 3 registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser()); 4 registerBeanDefinitionParser("annotation-driven", 5 new AnnotationDrivenBeanDefinitionParser()); 6 registerBeanDefinitionParser("jta-transaction-manager", 7 new JtaTransactionManagerBeanDefinitionParser()); 8}

       可以看到,这里的annotation-driven是在AnnotationDrivenBeanDefinitionParser中进行处理的,其parse()方法就是解析标签,并且注册相关bean的方法,如下是该方法的实现:

1public BeanDefinition parse(Element element, ParserContext parserContext) { 2 // 注册事务相关的监听器,如果某个方法标注了TransactionalEventListener注解, 3 // 那么该方法就是一个事务事件触发方法,即发生某种事务事件后,将会根据该注解的设置,回调指定 4 // 类型的方法。常见的事务事件有:事务执行前和事务完成(包括报错后的完成)后的事件。 5 registerTransactionalEventListenerFactory(parserContext); 6 String mode = element.getAttribute("mode"); 7 // 获取当前事务驱动程序的模式,如果使用了aspectj模式,则会注册一个AnnotationTransactionAspect 8 // 类型的bean,用户可以以aspectj的方式使用该bean对事务进行更多的配置 9 if ("aspectj".equals(mode)) { 10 registerTransactionAspect(element, parserContext); 11 } else { 12 // 一般使用的是当前这种方式,这种方式将会在Spring中注册三个bean,分别是 13 // AnnotationTransactionAttributeSource,TransactionInterceptor 14 // 和BeanFactoryTransactionAttributeSourceAdvisor,并通过Aop的方式实现事务 15 AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); 16 } 17 return null; 18}

       可以看到,对于事务的驱动,这里主要做了两件事:①注册事务相关的事件触发器,这些触发器由用户自行提供,在事务进行提交或事务完成时会触发相应的方法;②判断当前事务驱动的模式,有默认模式和aspectj模式,对于aspectj模式,Spring会注册一个AnnotationTransactionAspect类型的bean,用于用户使用更亲近于aspectj的方式进行事务处理;对于默认模式,这里主要是声明了三个bean,最终通过Aop的方式进行事务切入。下面我们看一下Spring是如何注册这三个bean的,如下是AopAutoProxyConfigurer.configureAutoProxyCreator的源码:

1public static void configureAutoProxyCreator(Element element, 2 ParserContext parserContext) { 3 // 这个方法主要是在当前BeanFactory中注册InfrastructureAdvisorAutoProxyCreator这个 4 // bean,这个bean继承了AbstractAdvisorAutoProxyCreator,也就是其实现原理与我们前面 5 // 讲解的Spring Aop的实现原理几乎一致 6 AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); 7 8 // 这里的txAdvisorBeanName就是我们最终要注册的bean,其类型就是下面注册的 9 // BeanFactoryTransactionAttributeSourceAdvisor,可以看到,其本质是一个 10 // Advisor类型的对象,因而Spring Aop会将其作为一个切面织入到指定的bean中 11 String txAdvisorBeanName = TransactionManagementConfigUtils 12 .TRANSACTION_ADVISOR_BEAN_NAME; 13 // 如果当前BeanFactory中已经存在了目标bean,则不进行注册 14 if (!parserContext.getRegistry().containsBeanDefinition(txAdvisorBeanName)) { 15 Object eleSource = parserContext.extractSource(element); 16 // 注册AnnotationTransactionAttributeSource,这个bean的主要作用是封装 17 // @Transactional注解中声明的各个属性 18 RootBeanDefinition sourceDef = new RootBeanDefinition( 19 "org.springframework.transaction.annotation.AnnotationTransactionAttributeSource"); 20 sourceDef.setSource(eleSource); 21 sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); 22 String sourceName = parserContext.getReaderContext() 23 .registerWithGeneratedName(sourceDef); 24 25 // 注册TransactionInterceptor类型的bean,并且将上面的封装属性的bean设置为其一个属性。 26 // 这个bean本质上是一个Advice(可查看其继承结构),Spring Aop使用Advisor封装实现切面 27 // 逻辑织入所需的所有属性,但真正的切面逻辑却是保存在其Advice属性中的,也就是说这里的 28 // TransactionInterceptor才是真正封装了事务切面逻辑的bean 29 RootBeanDefinition interceptorDef = 30 new RootBeanDefinition(TransactionInterceptor.class); 31 interceptorDef.setSource(eleSource); 32 interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); 33 registerTransactionManager(element, interceptorDef); 34 interceptorDef.getPropertyValues().add("transactionAttributeSource", 35 new RuntimeBeanReference(sourceName)); 36 String interceptorName = parserContext.getReaderContext() 37 .registerWithGeneratedName(interceptorDef); 38 39 // 注册BeanFactoryTransactionAttributeSourceAdvisor类型的bean,这个bean实现了 40 // Advisor接口,实际上就是封装了当前需要织入的切面的所有所需的属性 41 RootBeanDefinition advisorDef = 42 new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class); 43 advisorDef.setSource(eleSource); 44 advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); 45 advisorDef.getPropertyValues().add("transactionAttributeSource", 46 new RuntimeBeanReference(sourceName)); 47 advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); 48 if (element.hasAttribute("order")) { 49 advisorDef.getPropertyValues().add("order", element.getAttribute("order")); 50 } 51 parserContext.getRegistry().registerBeanDefinition(txAdvisorBeanName, advisorDef); 52 53 // 将需要注册的bean封装到CompositeComponentDefinition中,并且进行注册 54 CompositeComponentDefinition compositeDef = 55 new CompositeComponentDefinition(element.getTagName(), eleSource); 56 compositeDef.addNestedComponent( 57 new BeanComponentDefinition(sourceDef, sourceName)); 58 compositeDef.addNestedComponent( 59 new BeanComponentDefinition(interceptorDef, interceptorName)); 60 compositeDef.addNestedComponent( 61 new BeanComponentDefinition(advisorDef, txAdvisorBeanName)); 62 parserContext.registerComponent(compositeDef); 63 } 64}

       如此,Spring事务相关的标签即解析完成,这里主要是声明了一个BeanFactoryTransactionAttributeSourceAdvisor类型的bean到BeanFactory中,其实际为Advisor类型,这也是Spring事务能够通过Aop实现事务的根本原因。

3. 实现原理

       关于Spring事务的实现原理,这里需要抓住的就是,其是使用Aop实现的,我们知道,Aop在进行解析的时候,最终会生成一个Adivsor对象,这个Advisor对象中封装了切面织入所需要的所有信息,其中就包括最重要的两个部分就是PointcutAdivce属性。这里Pointcut用于判断目标bean是否需要织入当前切面逻辑;Advice则封装了需要织入的切面逻辑。如下是这三个部分的简要关系图:

Advisor

       同样的,对于Spring事务,其既然是使用Spring Aop实现的,那么也同样会有这三个成员。我们这里我们只看到了注册的Advisor和Advice(即BeanFactoryTransactionAttributeSourceAdvisor和TransactionInterceptor),那么Pointcut在哪里呢?这里我们查看BeanFactoryTransactionAttributeSourceAdvisor的源码可以发现,其内部声明了一个TransactionAttributeSourcePointcut类型的属性,并且直接在内部进行了实现,这就是我们需要找的Pointcut。这里这三个对象对应的关系如下:

Transaction

       这样,用于实现Spring事务的Advisor,Pointcut以及Advice都已经找到了。关于这三个类的具体作用,我们这里进行整体的上的讲解,后面我们将会深入其内部讲解其是如何进行bean的过滤以及事务逻辑的织入的。

  • BeanFactoryTransactionAttributeSourceAdvisor:封装了实现事务所需的所有属性,包括Pointcut,Advice,TransactionManager以及一些其他的在Transactional注解中声明的属性;
  • TransactionAttributeSourcePointcut:用于判断哪些bean需要织入当前的事务逻辑。这里可想而知,其判断的基本逻辑就是判断其方法或类声明上有没有使用@Transactional注解,如果使用了就是需要织入事务逻辑的bean;
  • TransactionInterceptor:这个bean本质上是一个Advice,其封装了当前需要织入目标bean的切面逻辑,也就是Spring事务是如果借助于数据库事务来实现对目标方法的环绕的。

4. 小结

       本文首先通过一个简单的例子讲解了Spring事务是如何使用的,然后讲解了Spring事务进行标签解析的时候做了哪些工作,最后讲解了Spring事务是如何与Spring Aop进行一一对应的,并且是如何通过Spring Aop实现将事务逻辑织入目标bean的。

5. 广告

       读者朋友如果觉得本文还不错,可以点击下面的广告链接,这可以为作者带来一定的收入,从而激励作者创作更好的文章,非常感谢!

在项目开发过程中,企业会有很多的任务、需求、缺陷等需要进行管理,CORNERSTONE 提供敏捷、任务、需求、缺陷、测试管理、WIKI、共享文件和日历等功能模块,帮助企业完成团队协作和敏捷开发中的项目管理需求;更有甘特图、看板、思维导图、燃尽图等多维度视图,帮助企业全面把控项目情况。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )