【Spring学习笔记 九】Spring声明式事务管理实现机制(下)

简介: 【Spring学习笔记 九】Spring声明式事务管理实现机制(下)

Spring声明式事务实现原理

声明式事务是建立在AOP之上的,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或加入一个事务,在执行完目标方法之后根据执行情况提交或回滚事务

声明式事务组成部分

Spring配置文件中关于事务配置总是由三个组成部分,分别是DataSource、TransactionManager代理机制这三部分,无论哪种配置方式,一般变化的只是代理机制这部分,DataSource、TransactionManager这两部分只是会根据数据访问方式有所变化,比如使用Hibernate进行数据访问时,DataSource实际为SessionFactory,TransactionManager的实现为HibernateTransactionManager

声明式事务的实现方式随着时间的演化过程分为四个阶段:

  1. 使用拦截器:基于TransactionInterceptor 类来实施声明式事务管理功能(Spring最初提供的实现方式);
  2. 使用Bean和代理:基于 TransactionProxyFactoryBean的声明式事务管理
  3. 使用tx标签配置的拦截器:基于tx和aop名字空间的xml配置文件(基于Aspectj AOP配置事务)
  4. 使用全注解实现:基于@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中配置一个注解,用到哪些配置方式在对应的方法上直接配就行了。当然配置和注解目前我认为还是不相伯仲的,配置可以集中化管理,注解可以快速上手,混合使用更好吧,我认为复杂实现还是使用配置好些,简单实现可以使用注解。所以总结而言就是:代码实现,配置取代代码,简化配置,注解取代配置

相关文章
|
5天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
16 2
|
2月前
|
Java 数据库连接 数据库
spring复习05,spring整合mybatis,声明式事务
这篇文章详细介绍了如何在Spring框架中整合MyBatis以及如何配置声明式事务。主要内容包括:在Maven项目中添加依赖、创建实体类和Mapper接口、配置MyBatis核心配置文件和映射文件、配置数据源、创建sqlSessionFactory和sqlSessionTemplate、实现Mapper接口、配置声明式事务以及测试使用。此外,还解释了声明式事务的传播行为、隔离级别、只读提示和事务超时期间等概念。
spring复习05,spring整合mybatis,声明式事务
|
2月前
|
Java 数据库连接 数据库
Spring基础3——AOP,事务管理
AOP简介、入门案例、工作流程、切入点表达式、环绕通知、通知获取参数或返回值或异常、事务管理
Spring基础3——AOP,事务管理
|
3月前
|
XML Java 数据库
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式
这篇文章是Spring5框架的实战教程,详细介绍了事务的概念、ACID特性、事务操作的场景,并通过实际的银行转账示例,演示了Spring框架中声明式事务管理的实现,包括使用注解和XML配置两种方式,以及如何配置事务参数来控制事务的行为。
Spring5入门到实战------15、事务操作---概念--场景---声明式事务管理---事务参数--注解方式---xml方式
|
3月前
|
Java 开发工具 Spring
Spring的Factories机制介绍
Spring的Factories机制介绍
70 1
|
3月前
|
Java Spring 开发者
掌握Spring事务管理,打造无缝数据交互——实用技巧大公开!
【8月更文挑战第31天】在企业应用开发中,确保数据一致性和完整性至关重要。Spring框架提供了强大的事务管理机制,包括`@Transactional`注解和编程式事务管理,简化了事务处理。本文深入探讨Spring事务管理的基础知识与高级技巧,涵盖隔离级别、传播行为、超时时间等设置,并介绍如何使用`TransactionTemplate`和`PlatformTransactionManager`进行编程式事务管理。通过合理设计事务范围和选择合适的隔离级别,可以显著提高应用的稳定性和性能。掌握这些技巧,有助于开发者更好地应对复杂业务需求,提升应用质量和可靠性。
44 0
|
4月前
|
Java 数据库连接 API
Spring事务管理嵌套事务详解 : 同一个类中,一个方法调用另外一个有事务的方法
Spring事务管理嵌套事务详解 : 同一个类中,一个方法调用另外一个有事务的方法
266 1
|
4月前
|
存储 安全 Java
实现基于Spring Cloud的分布式配置管理
实现基于Spring Cloud的分布式配置管理
|
4月前
|
安全 Java API
构建基于Spring Boot的REST API安全机制
构建基于Spring Boot的REST API安全机制
|
4月前
|
XML Java 关系型数据库
面试一口气说出Spring的声明式事务@Transactional注解的6种失效场景
面试一口气说出Spring的声明式事务@Transactional注解的6种失效场景
119 0