Spring声明式事务实现原理
声明式事务是建立在AOP之上的,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或加入一个事务,在执行完目标方法之后根据执行情况提交或回滚事务。
声明式事务组成部分
Spring配置文件中关于事务配置总是由三个组成部分,分别是DataSource、TransactionManager和代理机制这三部分,无论哪种配置方式,一般变化的只是代理机制这部分,DataSource、TransactionManager这两部分只是会根据数据访问方式有所变化,比如使用Hibernate进行数据访问时,DataSource实际为SessionFactory,TransactionManager的实现为HibernateTransactionManager
声明式事务的实现方式随着时间的演化过程分为四个阶段:
- 使用拦截器:基于
TransactionInterceptor
类来实施声明式事务管理功能(Spring最初提供的实现方式); - 使用Bean和代理:基于
TransactionProxyFactoryBean
的声明式事务管理 - 使用tx标签配置的拦截器:基于tx和aop名字空间的xml配置文件(基于Aspectj AOP配置事务)
- 使用全注解实现:基于@Transactional注解
编程式事务每次实现都要单独实现,但业务量大且功能复杂时,使用编程性事务无疑是痛苦的;而声明式事务不同,声明式事务属于非侵入性,不会影响业务逻辑的实现,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中,这种非侵入式的开发方式使得声明式事务管理下的业务代码不受污染,当然缺点也比较明显就是:最细粒度只能是作用到方法级别,无法做到像编程事务那样可以作用到代码块级别
声明式事务执行流程
首先Spring通过事务管理器(PlatformTransactionManager
的子类)创建事务,与此同时会把事务定义中的隔离级别、超时时间等属性根据配置内容往事务上设置,根据传播行为配置采取一种特定的策略,后面会谈到传播行为的使用问题,这是Spring根据配置完成的内容,只需配置,无须编码。
然后,启动开发者提供的业务代码,我们知道Spring会通过反射+动态代理的方式调度开发者的业务代码,业务代码执行的结果可能是正常返回或者产生异常返回,那么它给的约定是只要发生异常,并且符合事务定义类回滚条件的,Spring就会将数据库事务回滚,否则将数据库事务提交,这也是Spring自己完成的
整体的执行流程如下图所示:
Spring声明式事务实现方式
大多数情况下我们使用的是声明式事务,因为声明式事务可以解决80%以上的场景,而且对现有逻辑没有侵入性,所以这里就不再做编程式事务的相关实践,而声明式事务的演化过程也分为如下4种:
我们使用事务时充分利用AOP来实现,所以只介绍最后也是最新最常用的两种用法,项目也是基于之前的spring-myBatis的整合项目,所以这里用的事务管理器也是DataSourceTransactionManager
不使用事务会怎样
还是那句话,使用一个技术前一定是有需求催生的,所以不使用事务会发生什么具体的问题呢?数据准备还是使用我们之前的整合项目【Spring学习笔记 八】Spring整合MyBatis实现方式,我们要实现如下一个需求,新增一个人员到系统中后要把这个人员同步到别的系统中,也就是保证两个系统中的数据一致,我们来准备下代码环境:
数据库表
PersonDao
package com.example.spring_mybatis.dao; import com.example.spring_mybatis.model.Person; import java.util.List; public interface PersonDao { List<Person> getPersonList(); int addAndSendPerson(Person person); }
PersonServiceImpl
package com.example.spring_mybatis.serviceImpl; import com.example.spring_mybatis.daoImpl.PersonDaoImpl; import com.example.spring_mybatis.model.Person; import com.example.spring_mybatis.service.PersonService; import lombok.Data; import java.util.List; @Data public class PersonServiceImpl implements PersonService { private PersonDaoImpl personDaoImpl; @Override public List<Person> getPersonList() { List<Person> personList=personDaoImpl.getPersonList(); for (Person person : personList) { System.out.println(person); } return personList; } @Override public int addAndSendPerson(Person person) { int result=personDaoImpl.addPerson(person);//本地新增人员 this.sendMessage(person); //发送人员同步到下游系统 return result; } public void sendMessage(Person person){ System.out.println("人员新增到下游系统失败"+person); throw new RuntimeException(); } }
PersonServiceTest
@Test public void addAndSendPersonTest(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); //落地还是落地到某个具体的接口上了,所以我们一般是一个接口对应一个实现类 PersonService personService = (PersonService) applicationContext.getBean("personServiceImpl"); Person person=new Person(); person.setId(4); person.setUsername("wcong"); person.setAge(30); person.setEmail("111111@qq.com"); person.setPassword("111111"); person.setPhone(11111111); person.setHobby("跳远"); personService.addAndSendPerson(person); }
我们跑一下单元测试,我们的目的是保证人员数据一致,如果下游同步失败抛异常,我们本地也不应该新增数据成功,然而运行单元测试:
异常抛出了,下游系统数据未同步,但是人员本地落库却成功了:
这就是不增加事务会产生的问题,接下来我们用声明式事务解决下该问题。
基于Aspectj AOP配置实现
通过Aspectj AOP配置实现需要经过如下步骤
1 添加tx名字空间
基于AOP的配置首先需要引入相关约束,支持使用tx标签来进行事务处理:
xmlns:tx="http://www.springframework.org/schema/tx"
2 applicationContext.xml配置文件配置
然后我们在applicationContext.xml
配置文件中进行相关配置:
<!-- 加载数据库配置信息 --> <context:property-placeholder location="properties/db.properties" system-properties-mode="NEVER"/> <!-- 连接池对象 --> <bean id="myDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" > <property name="driverClassName" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </bean> <!-- 加载事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="myDataSource" /> </bean> <!-- <tx:advice>定义事务通知,用于指定事务属性,其中“transaction-manager”属性指定事务管理器,并通过<tx:attributes>指定具体需要拦截的方法 <tx:method>拦截方法,其中参数有: name:方法名称,将匹配的方法注入事务管理,可用通配符 propagation:事务传播行为, isolation:事务隔离级别定义;默认为“DEFAULT” timeout:事务超时时间设置,单位为秒,默认-1,表示事务超时将依赖于底层事务系统; read-only:事务只读设置,默认为false,表示不是只读; rollback-for:需要触发回滚的异常定义,可定义多个,以“,”分割,默认任何RuntimeException都将导致事务回滚,而任何Checked Exception将不导致事务回滚; no-rollback-for:不被触发进行回滚的 Exception(s);可定义多个,以“,”分割; --> <!--配置事务通知--> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!--配置哪些方法使用什么样的事务,配置事务的传播特性--> <!-- 拦截addAndSendPerson方法,事务传播行为为:REQUIRED:必须要有事务, 如果没有就在上下文创建一个 --> <tx:method name="addAndSendPerson" propagation="REQUIRED" isolation="READ_COMMITTED" timeout="20" read-only="false" no-rollback-for="" rollback-for="java.lang.Exception"/> <!-- 支持,如果有就有,没有就没有 --> <tx:method name="*" propagation="SUPPORTS"/> </tx:attributes> </tx:advice> <!--配置AOP--> <aop:config proxy-target-class="true"> <aop:pointcut id="txPointcut" expression="execution(* com.example.spring_mybatis.service..*.*(..))"/> <!--<aop:advisor>定义切入点,与通知,把tx与aop的配置关联,才是完整的声明事务配置 --> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/> </aop:config>
3 测试事务实现
然后我们调整测试方法新插入一条数据看是否能插入成功:
@Test public void addAndSendPersonTest(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); //落地还是落地到某个具体的接口上了,所以我们一般是一个接口对应一个实现类 PersonService personService = (PersonService) applicationContext.getBean("personServiceImpl"); Person person=new Person(); person.setId(5); person.setUsername("lisi"); person.setAge(30); person.setEmail("111111@qq.com"); person.setPassword("111111"); person.setPhone(11111111); person.setHobby("跳远"); personService.addAndSendPerson(person); }
单元测试结果如下:
然后我们再看数据库中,该数据并没有被add成功:
基于@Transactional注解实现
接下来我们看看通过注解如何实现:
@Transactional注解特性
关于@Transactional注解我们还需要了解一些配置属性:
@Transactional注解使用规范
@Transactional 注解有如下几种使用原则和注意点,防止我们使用时配置错误
- @Transactional 可以作用于接口、接口方法、类以及类方法上。当作用于类上时,该类的所有 public 方法都将具有该类型的事务属性,同时,我们也可以在方法级别使用该注解来覆盖类级别的定义。
- 虽然 @Transactional 注解可以作用于接口、接口方法、类以及类方法上,但是 Spring 建议不要在接口或接口方法上使用该注解,因为这只有在使用基于接口的代理时它才会生效。另外,@Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果在 protected、private 或者默认可见性的方法上使用@Transactional注解,这将被忽略,也不会抛出任何异常。
- 如果在接口、实现类或方法上都指定了@Transactional 注解,则优先级顺序为方法>实现类>接口;
关于注解的实现方式我们知道这些就够了
@Transactional注解实现步骤
基于配置的方式当然略显繁琐,我们再来尝试下基于注解的实现方式吧。
1 添加tx名字空间
基于注解的配置首先也需要引入相关约束,支持使用tx标签来进行事务处理:
xmlns:tx="http://www.springframework.org/schema/tx"
2 applicationContext.xml
配置文件配置
然后开启事务的注解支持:Spring 使用 BeanPostProcessor 来处理 Bean 中的标注,因此我们需要在配置文件中作如下声明来激活该后处理 Bean:
<!-- 开启事务控制的注解支持 --> <tx:annotation-driven transaction-manager="transactionManager"/>
MyBatis自动参与到Spring事务管理中,无需额外配置,只要org.mybatis.spring.SqlSessionFactoryBean
引用的数据源与DataSourceTransactionManager
引用的数据源一致即可,这样我们的applicationContext.xml
配置文件相关配置如下:
<!-- 开启事务控制的注解支持 --> <tx:annotation-driven transaction-manager="transactionManager"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="myDataSource" /> </bean>
4 PersonServiceImpl类的addAndSendPerson方法加注解
然后我们需要在PersonServiceImpl类上的方法加@Transactional 注解:
package com.example.spring_mybatis.serviceImpl; import com.example.spring_mybatis.daoImpl.PersonDaoImpl; import com.example.spring_mybatis.model.Person; import com.example.spring_mybatis.service.PersonService; import lombok.Data; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Data public class PersonServiceImpl implements PersonService { private PersonDaoImpl personDaoImpl; @Override public List<Person> getPersonList() { List<Person> personList=personDaoImpl.getPersonList(); for (Person person : personList) { System.out.println(person); } return personList; } @Override @Transactional public int addAndSendPerson(Person person) { int result=personDaoImpl.addPerson(person); this.sendMessage(person); return result; } public void sendMessage(Person person){ System.out.println("人员新增到下游系统失败"+person); throw new RuntimeException(); } }
5 测试事务实现
然后我们再进行单元测试:
然后看数据库发现数据没有被插入进行:
基于配置和基于注解实现对比
基于 <tx>
命名空间和基于 @Transactional
的事务声明方式各有优缺点。基于 <tx>
的方式,其优点是与切点表达式结合,功能强大。利用切点表达式,一个配置可以匹配多个方法,而基于 @Transactional
的方式必须在每一个需要使用事务的方法或者类上用 @Transactional
标注,尽管可能大多数事务的规则是一致的,但是对 @Transactional
而言,也无法重用,必须逐个指定。另一方面,基于 @Transactional
的方式使用起来非常简单明了,没有学习成本。开发人员可以根据需要,任选其中一种使用,甚至也可以根据需要混合使用这两种方式
总结一下
其实事务的实现方式也存在类似Spring整合MyBatis或者Spring AOP的实现演进方式类似,最开始我们使用一组API去实现功能;后来有个了Spring把这组API交给配置文件管理,使用最原生的方式实现,例如AOP使用原生动态代理实现、事务最初用的是基于拦截器的实现方式;再后来Spring发现可以把这些经典实现规范化为一组标签,用于简化配置或者增强其通用能力,这样就为其规范了标准的命名空间,大大简化了配置也让我们更明白这组配置的作用,例如<aop>
标签就是用来实现AOP的,<tx>
标签就是用来实现事务的;最终因为有了注解所有Spring认为干脆连这个配置都不需要了,直接在XML中配置一个注解,用到哪些配置方式在对应的方法上直接配就行了。当然配置和注解目前我认为还是不相伯仲的,配置可以集中化管理,注解可以快速上手,混合使用更好吧,我认为复杂实现还是使用配置好些,简单实现可以使用注解。所以总结而言就是:代码实现,配置取代代码,简化配置,注解取代配置