第 5 章 Spring 的事务管理
通过上一章的学习,大家已经掌握了如何使用 Spring 来操作数据库,但是在实际开发中, 操作数据库时还会涉及事务管理问题,为此 Spring 提供了专门用于事务处理的 APl。 Spring 的事务管理简化了传统的事务管理流程,并且在一定程度上减少了开发者的工作量。 本章将针对 Spring 的事务管理功能进行详细讲解。
Spring 事务管理概述
- 事务管理的核心接口
在 Spring 的所有 JAR 包中,包含一个名为 spring-tx-4.3.6.RELEASE 的 JAR 包,该包就是 Spring 提供的用于事务管理的依赖包。 在该 JAR 包的 org.springframework.transaction 包中,我们 可以找到 3 个接口文件 PlatformTransactionManager、 TransactionDefinition 和 TransactionStatusI 如图所示。
在图中,方框标注的 3 个接口文件就是 Spring 事务管理所涉及的 3 个核心接口,接下 来对这 3 个接口的作用分别进行讲解。
- PlatformTransactionManager
PlatformTransactionManager 接口是 Spring 提供的平台事务管理器,主要用于管理事务。 该接口中提供了 3 个事务操作的方法,具体如下。
- TransactionStatusgetTransaction ( TransactionDefinition definition ):用于获取事务状 态信息。
- void commit ( TransactionStatus status ):用于提交事务。
- void rollback ( TransactionStatus status ):用于回滚事务。
在上面的 3 个方法中,getTransaction ( TransactionDefinition.definition )方法会根据 TransactionDefinition 参数返回一个 TransactionStatus 对象,TransactionStatus 对象就表示一 个事务,它被关联在当前执行的线程上。
PlatformTransactionMangger 接口只是代表事务管理的接口,它并不知道底层是如何管理事 务的,它只需要事务管理提供上面的 3 个方法,但具体如何管理事务则由它的实现类来完成。
PlatformTransactionManager 接口有许多不同的实现类,常见的几个实现类如下。
- org.springframework.jdbc.datasource.DataSourceTransactionManager: 用于配置 JDBC 数 据源的事务管理器。
- org.springframework.orm.hibernate4. HibernateTransactionManager :用于配置 Hibernate 的事务管理器。
- org.springframework. transaction.jta.JtaTransactionManager: 用于配置全局事务管 理器。
当底层采用不同的持久层技术时,系统只需使用不同PlatformTransactionManager 实现 类即可。
- TransactionDefinìtion
TransactionDefinition 接口是事务定义(描述)的对象,该对象中定义了事务规则,并提供 了获取事务相关信息的方法,具体如下。
- String getName(): 获取事务对象名称。
- int getlsolationLevel(): 获取事务的隔离级别。
- int getPropagationBehavior(): 获取事务的传播行为。
- int getTimeout(): 获取事务的超时时间。
- boolean isReadOnly(): 获取事务是否只读。
上述方法中,事务的传播行为是指在同一个方法中,不同操作前后所使用的事务。 传播行为有很多种,具体如下表所示。
属性名称 值 描述 PROPAGATION_REQUIRED REQUIRED 表示当前方法必须运行在一个事务环境当中,如果当刚方法己处于事务环境中,则可以直接使用该方法;否则会开启一个新事务后执行该方法 PROPAGATION_SUPPORTS SUPPORTS 如果当刚方法处于事务环境中,则使用当前事务,否则不使 SUPPORTS 用事务 PROPAGATION_MANDATORY MANDATORY 表示调用该方法的线程必须处于当前事务环境中,否则将抛异常 PROPAGATION_REQUIRES_NEW REQUIRES_NEW 要求方法在新的事务环境中执行。如果当刚方法己在事务环境中, 则先暂停当前事务,在启动新的事务后执行该方法,如果当前方法不在事务环境中,则会启动一个新的事务后执行方法 PROPAGATION_NOT SUPPORTED NOT SUPPORTED 不支持当刚事务,总是以非事务状态 、执行。 如果调用该万法的线 程处于事务环境中,则先暂停当前事务,然后执行该方法 PROPAGATION_NEVER NEVER 不支持当前事务。 如果调用该方法的线程处于事务环境中,将抛异常 PROPAGATION_NESTED NESTED 即使当刚执行的方法处于事务环境中,依然会启动一个新的事务, 并且方法在嵌套的事务里执行;即使当前执行的方法不在事务环境中,也会启动一个新事务,然后执行该方法 在事务管理过程中,传播行为可以控制是否需要创建事务以及如何创建事务,通常情况下, 数据的查询不会影响原数据的改变,所以不需要进行事务管理,而对于数据的插入、更新和删除 操作,必须进行事务管理。 如果没有指定事务的传播行为, Spring 默认传播行为是 REQUIRED。
- TransactionStatus
TransactionStatus 接口是事务的状态,它描述了某一时间点上事务的状态信息。 该接口中包含 6 个方法,具体如下。
- void flushO: 刷新事务。
- boolean hasSavepoint(): 获取是否存在保存点。
- boolean isCompleted(): 获取事务是否完成。
- boolean isNewTransaction(): 获取是否是新事务。
- boolean isRollbackOnly(): 获取是否回滚。
- void setRollbackOnly(): 设置事务回滚。
- 事务管理的方式
Spring 中的事务管理分为两种方式:一种是传统的编程式事务管理,另一种是声明式事务 管理。
- 编程式事务管理:是通过编写代码实现的事务管理,包括定义事务的开始、正常执行后的 事务提交和异常时的事务回滚。
- 声明式事务管理:是通过 AOP 技术实现的事务管理,其主要思想是将事务管理作为一个 "切面"代码单独编写,然后通过 AOP 技术将事务管理的"切面"代码织入到业务目标类中。
声明式事务管理最大的优点在于开发者无须通过编程的方式来管理事务,只需在配置文件中 进行相关的事务规则声明,就可以将事务规则应用到业务逻辑中 。 这使得开发人员可以更加专注 于核心业务逻辑代码的编写,在一定程度上减少了工作量,提高了开发效率,所以在实际开发中, 通常都推荐使用声明式事务管理。 本书主要讲解的就是 Spring 的声明式事务管理。
声明式事务管理
Spring 的声明式事务管理可以通过两种方式来实现,一种是基于 XML 的方式,另一种是基 于 Annotation 的方式。 接下来的两个小节中,将对这两种声明式事务管理方式进行详细讲解。
- 基于 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>元素及其子元素如下图所示。
在上图中,配置<tx:advice>元素的重点是配置<tx:method>子元素,图中使用灰色标注 的几个属性是<tx:method>元素中的常用属性。 关于<tx:method>元素的属性描述如下表所示。
属性名称 值 name 该属性为必选属性,它指定了与事务属性相关的方法名。其属性值支持使用通配符,如'*'、'get*'、 'handle*'、 '*Order'等 propagation 用于指定事务的传播行为,其属性值就是上小节的表中的值,巴的默认值为 REQUIRED isolation 该属性用于指定事务的隔离级别,莫属性值可以为 DEFAULT、 READ_UNCOMMITTED、 READ_COMMITTED、 REPEATABLE_READ 和 SERIALlZABLE ,真默认值为 DEFAULT read-only 该属性用于指定事务是否只i卖,真默认值为 false timeout 该属性用于指定事务超时的时间,其默认值为-1 ,即永不超时 rollback-for 该属性用于指定触发事务回滚的异常类,在指定多个异常类肘,异常类之间以英文逗号分隔 no-rollback-for 该属性用于指定不触发事务回滚的异常类,在指定多个异常类时,异常类之间以英文逗号分隔 了解了如何在 XML 文件中配置事务后,接下来通过一个案例来演示如何通过 XML 方式来实 现 Spring 的声明式事务管理。 本案例以上一章的项目代码和数据表为基础,编写一个模拟银行转账的程序,要求在转账时通过 Spring 对事务进行控制,其具体实现步骤如下。
( 1 )在 Eclipse 中,创建一个名为 spring05 的 Web 项目,在项目的 lib 目 录中导入 chapter04 项目中的所有 JAR 包,并将 AOP 所需 JAR 包也导入到 lib 目录中。 导入后的 lib 目 录如图所示。
(2 )将 spring04 项目中的代码和配置文件复制到 spring05 项目的 src 目录下,并在 AccountDao 接口中,创建一个转账方法 transfer() ,其代码如下所示。
//转账 public void transfer(String outUser, String inUser, Double money);
(3 )在其实现类 AccountDaolmpl 中实现 transfer()方法,编辑后的代码如下所示。
/** * 转账 * inUser:收款人 * outUser:汇款人 * money:收款金额 */ 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:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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-4.3.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:3306/spring" /> <!-- 连接数据库的用户名 --> <property name="username" value="root" /> <!-- 连接数据库的密码 --> <property name="password" value="root" /> </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.neuedu.jdbc.AccountDaoImpl"> <!-- 将 jdbcTernplate 注入到 accountDao 实例中 --> <property name="jdbcTemplate" ref="jdbcTemplate"></property> </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.neuedu.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.neuedu.jdbc 包中,创建测试类 TransactionTest ,并在类中编写测试方法 xmlTest() ,文件如下所示。package com.neuedu.jdbc; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; //测试类 public class TransactionTest { @Test public void xmlTest() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); //获取 AccountDao 实例 AccountDao accountDao = (AccountDao)applicationContext.getBean("accountDao"); //调用实例中转账方法 accountDao.transfer("haha" , "hehe" , 100.0); //输出提示信息 System.out.println("转账成功!"); } }
在上述文件中,获取了 AccountDao 实例后,调用了实例中的转账方法,由 haha 向 hehe 的账户中转入 100 元。 如果在配置文件中所声明的事务代码能够起作用,那么在整个转账方法执行完毕后, haha 和 hehe 的账户余额应该都是原来的数值。 在执行转账操作前,先查看 account 表中的数据,如图所示。
从图可以看出,此时 haha 的账户余额是 3000 ,而 hehe 的账户余额是 4000。 执行完前面文件中的测试方法后, Junit 的控制台的显示结果如图所示。
从图可以看到, Junit 控制台中报出了 "/by zero" 的算术异常信息。 此时如果再次查询数据表 account,会发现表中 haha 和 hehe 的账户余额并没有发生任何变化(与测试之前图中的显示结果一样),这说明 Spring 中的事务管理配置已经生效。
- 基于 Annotation 方式的声明式事务
Spring 的声明式事务管理还可以通过 Annotation (注解)的方式来实现。 这种方式的使用 非常简单,开发者只需做两件事情:
① 在 Spring 容器中注册事务注解驱动,其代码如下。
② 在需要使用事务的 Spring Bean 类或者 Bean 类的方法上添加注解@Transactional。如果 将注解添加在 Bean 类上,则表示事务的设置对整个 Bean 类的所有方法都起作用;如果将注解 添加在 Bean 类中的某个方法上,则表示事务的设置只对该方法有效。
使用@Transactional 注解时,可以通过其参数配置事务详情。 @Transactional 注解可配置的 参数信息如下表所示。
参数名称 描述 value 用于指定需要使用的事务管理器,默认为"",真别名为 transactionManager transactionManager 指定事务的限定符值,可用于确定目标事务管理器,匹配特定的限定值( 或者 Bean 的 name 值) ,默认为"",真别名为 value isolation 用于指定事务的隔离级别,默认为 Isolation.DEFAULT ( 即底层事务的隔离级别 ) noRollbackFor 用于指定遇到特定异常时强制不回滚事务 noRollbackForClassName 用于指定遇到特定的多个异常时强制不回滚事务。 莫属性值可以指定多个异常类名 propagation 用于指定事务的传播行为,默认为 Propagation.REQUIRED read-only 用于指定事务是否只i卖, 默认为 false rollbackFor 用于指定遇到特定异常时强制回滚事务 rollbackForClassName 用于指定遇到特定的多个异常时强制回滚事务。 莫属性值可以指定多个异常类包 timeout 用于指定事务的超时时长,默认为 TransactionDefinition.TIMEOUT_DEFAULT (即底层事务系统的默认时间) 从上表可以看出, @Transactional 注解与<tx:method>元素中的事务属性基本是对应的,并且其含义也基本相似。
为了让大家更加清楚地掌握@Transactional 注解的使用,接下来对上一小节的案例进行修改,以 Annotation 方式来实现项目中的事务管理,具体实现步骤如下。
( 1 )在 src 目录下,创建一个 Spring 配置文件 applicationContext-annotation.xml ,在该文件中声明事务管理器等配置信息,文件如下所示。<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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-4.3.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:3306/spring" /> <!-- 连接数据库的用户名 --> <property name="username" value="root" /> <!-- 连接数据库的密码 --> <property name="password" value="root" /> </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.neuedu.jdbc.AccountDaoImpl"> <!-- 将 jdbcTernplate 注入到 accountDao 实例中 --> <property name="jdbcTemplate" ref="jdbcTemplate"></property> </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>
与基于 XML 方式的配置文件相比 ,上述文件通过注册事务管理器驱动,替换了上一节文件中的第 5 步编写通知和第 6 步编写 aop ,这样大大减少了配置文件中的代码量。
需要注意的是,如果案例中使用了注解式开发,则需要在配置文件中开启注解处理器,指定扫描哪些包下的注解。 这里没有开启注解处理器是因为在配置文件中已经配置了 AccountDaolmpl 类的 Bean ,而@Transactional 注解就配置在该 Bean 类中 , 所以可以直接生效。
( 2 ) 在 AccountDaolmpl 类的 transfer()方法上添加事务注解,添加后的代码如下所示。@Transactional(propagation=Propagation.REQUIRED, isolation=Isolation.DEFAULT,readOnly=false) 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 注解, 并且使用注解的参数配置了事务详情, 各个参数之间要用英文逗号 " , "进行分隔。
小提示:在实际开发中 ,事务的配置信息通常是在 Spring 的配置文件中完成的 ,而在业务层类上只需 使用@Transactional 注解即可,不需要配置@Transactional 注解的属性。
( 3 ) 在 TransactionTest 类中, 创建测试方法 annotationTest() , 编辑后的代码如下所示。@Test public void annotationTest() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); //获取 AccountDao 实例 AccountDao accountDao = (AccountDao)applicationContext.getBean("accountDao"); //调用实例中转账方法 accountDao.transfer("haha" , "hehe" , 100.0); //输出提示信息 System.out.println("转账成功!"); }
从上述代码可以看出,与 XML 方式的测试方法相比,该方法只是对配置文件的名称进行了修改。 程序执行后,会出现与 XML 方式同样的执行结果,这里就不再做重复演示,大家可自行测试。
本章小结
本章主要对 Spring 中的事务管理进行了详细讲解。 首先讲解了 Spring 事务管理所涉及的 3 个核心接口,然后对 Spring 中事务管理的两种方式进行了介绍,最后通过案例分别对基于 XML 方式和基于 Annotation 方式的声明式事务处理的使用进行了详细讲解。 通过本章的学习,大家可以对 Spring 的事务管理知识有一定的了解,并能够掌握 Spring 声明式事务管理的使用。