昔洛 的个人博客

Bug不空,誓不成佛

  menu
70 文章
14633 浏览
2 当前访客
ღゝ◡╹)ノ❤️

第五篇 Spring 的事务管理

一、Spring 事务管理概述

1、事务管理的核心接口

    在 Spring 的所有JAR包中,包含一个名为Spring-tx-4.3.6.RELEASE 的JAR包,该包就是Spring提供的用于事务管理的依赖包。在该JAR包的org.springframework.transaction包中,可以找到3个接口文件 PlatformTransactionManagerTransactionDefinitionTransactionStatus
1.PlatformTransactionManager
    PlatformTransactionManager 接口是Spring提供的平台事务管理器,主要用于管理事务。该接口中提供了3个实务操作的方法,具体如下:

1.PlatformTransactionManager

    PlatformTransactionManager 接口是Spring提供的平台事务管理器、主要用于管理事务。该接口中提供了3个事务操作的方法,如下:

  • TransactionStatus getTransaction(TransactionDefinition definition):用于获取事务状态信息。
  • void commit(TransactionStatus status):用于提交事务。
  • void rollback(TransactionStatus status):用于回滚事务

在上面3个方法中,getTransaction(TransactionDefinition definition)方法会根据
TransactionDefinition参数返回一个TransactionStatus对象,TransactionStatus对象就表示一个事务,它被关联在当前执行的线程上。
PlatformTransactionManager 接口只是代表事务管理的接口,它并不知道底层是如何管理事务的,它只需要事务管理提供上面的3个方法,但具体如何管理事务则由它的实现类来完成。
PlatformTransactionManager 接口有许多不同的实现类,常见的几个实现类如下:

  • org.springframework.jdbc.datasource.DataSourceTransactionManager:用于配置JDBC数据源的事务管理器
  • org.springframework.hibernate4.HibernateTransactionManager:用于配置Hibernate的事务管理器。
  • org.springframework.transaction.jta.JtaTransactionManager:用于配置全局事务管理器。

当底层采用不同的持久层技术时,系统只需使用不同的PlatformTransactionManager 实现类即可。

2.TransactionDefinition

    TransactionDefinition 接口是事务定义(描述)的对象,该对象中定义了事务规则,并提供了获取事务相关信息的方法,具体如下:

  • String getName():获取事务对象名称。
  • int getIsolationLevel():获取事务的隔离级别
  • int getPropagationBehavior():获取事务的传播行为
  • int getTimeout():获取事务的超时时间。
  • boolean isReadOnly():获取事务是否只读。

上述方法中,事务的传播行为是指在同一个方法中,不同操作前后所使用的事务。传播行为有很多种,具体如下表:

属性名称描述
PROPAGATION_REQUIREDREQUIRED表示当前方法必须运行在一个事物环境当中,如果当前方法已处于事物环境中,则可以直接使用该方法;否则会开启一个新事务后执行该方法
PROPAGATION_SUPPORTSSUPPORTS如果当前方法处于事物环境中,则使用当前事务,否则不使用事务
PROPAGATION_MANDATORYMANDATORY表示调用该方法的第线程必须处于当前事务环境中,否则将抛出异常
PROPAGATION_REQUIRES_NEWREQUIRES_NEW要求方法在新的事务环境中执行。如果当前方法已在事物环境中,则先暂停当前事务,在启动新的事务后执行该方法;如果当前方法不在事物环境中,则先暂停当前事务,然后执行该方法
PROPAGATION_NOT_SUPPORTEDNOT_SUPPORTED不支持当前事务,总是 以非事务状态执行。如果调用该方法的线程处于事物环境中,则先暂停当前事务,然后执行该方法
PROPAGATION_NEVERNEVER不支持当前事务。如果调用该方法的线程处于事物环境中,将抛异常
PROPAGATION_NESTEDNESTED即使当前执行的方法处于事物环境中,依然会启动一个新的事务,并且方法在嵌套的事务里执行;即使当前执行的方法不在事务环境中,也会启动一个新事务,然后执行该方法

    在事务管理过程中,传播行为可以控制是否需要创建事务以及如何创建事务,通常情况下,数据的查询不会影响原数据的改变,所以不需要进行事务管理,而对于数据的插入、更新和删除操作,必须进行事务管理。如果没有指定事务的传播行为,Spring默认传播行为是REQUIRED。

3.TransactionStatus

    TransactionStatus 接口是事务的状态,它描述了某一时间点上事务的状态信息。该接口中包含6个方法,具体如下:

  • void flush():刷新事务。
  • boolean hasSavepoint():获取是否存在保存点。
  • boolean isCompleted():获取事务事否完成。
  • boolean isNewTransaction():获取是否是新事物。
  • boolean isRollbackOnly():获取是否回滚。
  • void setRollbackOnly():设置事务回滚。

2、事务管理的方式

    Spring 中的事务管理分为两种方式:一种是传统的编程式事务管理,另一种是声明式事务管理。

  • 编程式事务管理:是通过编写代码实现的事务管理,包括定义事务的开始、正常执行后事务提交和异常时的事务回滚。
  • 声明式事务管理:是通过 AOP 技术实现的事务管理,其主要思想是将事务管理作为一个“切面”代码单独编写,然后通过 AOP 技术将事务管理的“切面”代码织入到业务目标类中。

声明式事务管理最大的有点在于开发者无需通过编程的方式来管理事务,只需在配置文件中进行相关的事务规则声明,就可以将事务规则应用到业务逻辑中。

二、声明式事务管理

1、基于 XML 方式的声明式事务

    基于 XML 方式的声明式事务管理是通过在配置文件中配置事务规则的相关声明来实现的。Spring 2.0 以后,提供了tx命名空间来配置事务,tx命名空间下提供了<tx:advice>元素来配置事务的通知(增强处理)。当使用<tx:advice>元素配置了事务的增强处理后,就可以通过编写的AOP配置,让Spring自动对目标生成代理。
    配置<tx:advice>元素时,通常需要指定id和transaction-manager属性,其中id属性是配置文件中的唯一标识,transaction-manager属性用于指定事务管理器。除此之外,还需要配置一个<tx:attributes>子元素,盖子元素可通过配置多个<tx:method>子元素来配置执行事务的细节。<tx:advice>元素及其子元素如图所示:
image.png

图中灰色标注的几个属性是<tx:method>元素中的常用属性。
关于<tx:method>元素的属性描述如下:

属性名称描述
name该属性为必选属性,它指定了与事务属性相关的方法名。其属性值支持使用通配符,如'*'、'get*'、'handle*'、'*Order'等
propagation用于指定事务的传播行为,其属性值就是表(传播行为的种类)的值,它的默认值为REQUIRED
isolation该属性用于指定事务的隔离级别,其属性值可以为DEFAULT、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ 和 SERIALIZABLE,其默认值为DEFAULT
read-only该属性用于指定事务是否只读,其默认值为false
timeout该属性用于指定事务超时的时间,其默认值为-1,即用不超时
rollback-for该属性用于指定触发事务回滚的异常类,在指定多个异常类时,异常类之间以英文逗号分隔
no-rollback-for该属性用于指定不出发事务回滚的异常类,在指定多个异常类时,异常类之间以英文逗号分隔

接下来用一个简单的案例来演示,以上一篇的项目代码和数据表为基础,编写一个模拟银行转账的程序,要求在转账时通过Spring对事物进行控制,具体步骤如下:
(1)在Eclipse 中,创建一个Web项目,在项目的lib目录中导入第4篇项目中的所有jar包,并将AOP所需JAR包也导入到lib目录中。导入后的包如下所示:
image.png
(2)将第四篇的项目的代码和配置文件复制到当前项目的src目录下,并在AccountDao接口中,创建一个转账方法transfer(),代码如下:

//转账
public void transfer(String outUser,String inUser,Double money);

(3)在实现类AccountDaoImpl中实现transfer()方法,编辑后代码如下:

/**

 * 转账
 * inUser:收款人
 * outUser:汇款人
 * money:收款金额
 */
@Override
public void transfer(String outUser, String inUser, Double money) {
	//收款时,收款用户的余额=现有余额+所汇金额
	this.jdbcTemplate.update("update account set balance = balance +? "
			+ "where username = ?",money,inUser);
	//模拟系统运行时的突发性问题
	int i = 1/0;
	//汇款时,汇款用户的余额=现有余额-所汇余额
	this.jdbcTemplate.update("update account set balance = balance-? "
			+ "where username = ?",money,outUser);
}

上述代码中,使用了两个update()方法对account表中的数据执行收款和汇款的更新操作。在两个操作之间,添加了一行代码"int i=1/0;" 来模拟系统运行时的突发性问题。如果没有事务控制,那么在转账后操作执行后,收款用户的余额会增加,而汇款用户的余额会因为系统出现问题而不变,这显然是有问题的;如果增加了事务控制,那么在转账操作执行后,收款用户余额和汇款用户的余额在问题出现前后都应该保持不变。
(4)修改配置文件applicationContext.xml,添加命名空间并编写事务管理的相关配置代码,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-4.3.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    <!-- 1.配置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    	<!-- 数据库驱动 -->
    	<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    	<!-- 连接数据库的url -->
    	<property name="url" value="jdbc:mysql://localhost/spring"/>
    	<!-- 连接数据库的用户名 -->
    	<property name="username" value="root"/>
    	<!-- 连接数据库的密码 -->
    	<property name="password" value="123456"/>
    </bean>
    <!-- 2.配置JDBC模板 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    	<!-- 默认必须使用数据源 -->
    	<property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- 3.定义id为accountDao的Bean -->
    <bean id="accountDao" class="com.cn.jdbc.AccountDaoImpl">
    	<!-- 将jdbcTemplate注入到accountDao实例中 -->
    	<property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>
    <!-- 4.事务管理器,依赖于数据源 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    	<property name="dataSource" ref = "dataSource"/>
	</bean>
	<!-- 5.编写通知:对事物进行增强(通知),需要编写对切入点和具体执行事务细节 -->
	<tx:advice id="txAdvice" transaction-manager="transactionManager">
		<tx:attributes>
			<!-- name:	*表示任意方法名称 -->
			<tx:method name="*" propagation="REQUIRED" isolation="DEFAULT" read-only="false"/>
		</tx:attributes>
	</tx:advice>
	<!-- 6.编写AOP,让Spring自动对目标生成代理,需要使用AspectJ的表达式 -->
	<aop:config>
		<!-- 切入点 -->
		<aop:pointcut expression="execution(* com.it.jdbc.*.*(...))" id="txPointCut"/>
		<!-- 切面:将切入点与通知整合 -->
		<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
	</aop:config>
</beans>

上述代码中,首先启用了Spring配置文件的aop、tx和context 3个命名空间(从配置数据源到声明事务管理的部分都没有变化),然后定义了id为transactionManager的事务管理器,接下来通过编写的通知来声明事务,最后通过声明AOP的方式让Spring自动生成代理。
(5)在com.cn.jdbc包中,创建测试类TransactionTest,并在类中编写测试方法,xmlTest(),代码如下:

package com.cn.jdbc;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TransactionTest {
	@Test
	public void xmlTest() {
		ApplicationContext appc = new ClassPathXmlApplicationContext("applicationContext.xml");
		//获取AccountDao实例
		AccountDao accountDao = appc.getBean("accountDao",AccountDao.class);
		//调用实例中的转账方法
		accountDao.transfer("Jack", "Rose", 100.0);
		//输出信息
		System.out.println("转账成功!");
	}
}

上面代码中,获取了AccountDao实例后,调用了实例中的转账方法,由Jack向Rose的账户中转入100元。如果在配置文件中所声明的事务代码能够起作用,那么整个转账方法执行完毕后,Jack 和 Rose的账户余额都是原来的数值
执行转账钱的account中的数据:
image.png
执行后的数据:
image.png
此时Junit控制台有如下报错信息:
image.png
从图中可以看出Junit控制台报出了"/by zero"的算术异常信息。而此时数据表account没有发生任何变化,这说明Spring中的事务管理配置已经生效。

2.基于Annotation 方式的声明式事务

    Spring 的声明式事务管理还可以通过Annotation(注解)的方式来实现。这种方式的使用非常简单,开发者只需做两件事:
① 在Spring容器中注册事务注解驱动,代码如下:
<tx:annotation-driven transaction-manager="transactionManager"/>
② 在需要使用事务的Spring Bean 类或者Bean类的方法上添加注解@Transactional注解可配置的参数信息如下表所示:

参数名称描述
value用于指定需要使用的事务管理器,默认为"",其别名为transactionManager
transactionManager指定事务的限定符值,可用于确定目标事务管理器,匹配特定的限定值(或者Bean 的 name 值),默认为"",其别名为value
isolation用于指定事务的隔离级别,默认为Isolation.DEFAULT(即底层事务的隔离级别)
noRollbackFor用于指定遇到特定异常时强制不回滚事务
noRollbackForClassName用于指定遇到特定的多个异常时强制不回滚事务。其属性值可以指定多个异常类名
propagation用于指定事务的传播行为,默认为Propagation.REQUIRED
read-only用于指定事务是否为只读,默认为false
rollbackFor用于指定遇到特定异常时强制回滚事务
rollbackForClassName用于指定遇到特定的多个异常时强制回滚事务。其属性值可以指定多个异常类名
timeout用于指定事务的超时时长,默认为TransactionDefinition.TIMEOUT_DEFAULT(即底层事务系统的默认时间)

从表中可以看出,@Transactional注解与<tx:method>元素中的事务属性基本是对应的,并且其含义也基本相似。
简单的Annotation方式实现项目中的事务管理,具体实现步骤如下:
(1)在src目录下创建一个Spring配置文件applicationContext-annotation.xml,代码如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-4.3.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    <!-- 1.配置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    	<!-- 数据库驱动 -->
    	<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    	<!-- 连接数据库的url -->
    	<property name="url" value="jdbc:mysql://localhost/spring"/>
    	<!-- 连接数据库的用户名 -->
    	<property name="username" value="root"/>
    	<!-- 连接数据库的密码 -->
    	<property name="password" value="123456"/>
    </bean>
    <!-- 2.配置JDBC模板 -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    	<!-- 默认必须使用数据源 -->
    	<property name="dataSource" ref="dataSource"/>
    </bean>
    <!-- 3.定义id为accountDao的Bean -->
    <bean id="accountDao" class="com.cn.jdbc.AccountDaoImpl">
    	<!-- 将jdbcTemplate注入到accountDao实例中 -->
    	<property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>
    <!-- 4.事务管理器,依赖于数据源 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    	<property name="dataSource" ref = "dataSource"/>
	</bean>
	<!-- 5.注册事务管理器驱动 -->
	<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

需要注意的是,如果案例中使用了注解式开发,则需要在配置文件中开启注解处理器,指定扫描哪些包下的注解。这里没有开启注解处理器是因为在配置文件中已经配置了AccountDaoImpl类的Bean,而@Transactional 注解就配置在该Bean类中,所以可以直接生效。
(2)在AccountDaoImpl类的transfer()方法上添加事务注解,代码如下:

@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT,readOnly = false)
@Override
public void transfer(String outUser, String inUser, Double money) {
	//收款时,收款用户的余额=现有余额+所汇金额
	this.jdbcTemplate.update("update account set balance = balance +? "
			+ "where username = ?",money,inUser);
	//模拟系统运行时的突发性问题
	int i = 1/0;
	//汇款时,汇款用户的余额=现有余额-所汇余额
	this.jdbcTemplate.update("update account set balance = balance-? "
			+ "where username = ?",money,outUser);
}

上述方法已经添加了@Transactional注解,并且使用注解的参数配置了事务详情,各个参数之间要用英文逗号","进行分隔。
Tips:在实际开发中,事务的配置信息通常是在Spring的配置文件中完成的,而在业务层类上只需使用@Transactional注解即可,不需要配置@Transactional注解的属性
(3)在TransactionTest类中,创建测试方法annotationTest(),代码如下:

@Test
public void annotationTest() {
	ApplicationContext appc = new ClassPathXmlApplicationContext("applicationContext-annotation.xml");
	//获取AccountDao实例
	AccountDao accountDao = (AccountDao)appc.getBean("accountDao");
	//调用实例中的转账方法
	accountDao.transfer("Jack", "Rose", 100.0);
	//输出信息
	System.out.println("转账成功!");
}

程序运行结果同上面一样account表没有发生任何变化同样Junit报出算术异常。

内事不懂问百度,外事不懂问谷歌~