第5章Spring的事务管理 学习目的与要求 本章主要介绍了Spring框架所支持的事务管理,包括编程式事务管理和声明式事务 管理。通过本章的学习,要求读者掌握声明式事务管理,了解编程式事务管理。 本章主要内容 .Spring的数据库编程 .编程式事务管理 .声明式事务管理 在数据库操作中,事务管理是一个重要的概念。例如,银行转账,当从A账户向B 账户转1000元后,银行的系统将从A账户上扣除1000元,而在B账户上加1000元,这 是正常处理的结果。 一旦银行系统出错了怎么办?这里假设发生两种情况: (1)A账户少了1000元,但B账户却没有多1000元。 (2)B账户多了1000元钱,但A账户却没有被扣钱。 客户和银行都不愿意发生上面两种情况。那么有没有措施保证转账顺利进行?这种措 施就是数据库事务管理机制。 Spring的事务管理简化了传统的数据库事务管理流程,提高了开发效率。在学习事务 管理之前,需要了解Spring的数据库编程。 5.1 Spring的数据库编程 数据库编程是互联网编程的基础,Spring框架为开发者提供了JDBC模板模式,即 jdbcTemplate,它可以简化许多代码,但在实际应用中jdbcTemplate并不常用,在工作中 更多的是使用Hibernate框架和MyBatis框架进行数据库编程。 本节简要介绍Spring jdbcTemplate的使用方法,对于MyBatis框架的相关内容将在第 14章详细介绍,对于Hibernate框架本书不再涉及,需要的读者可以查阅Hibernate框架的 相关知识。 XX5.1.1 Spring JDBC的配置 本节Spring的数据库编程主要使用Spring JDBC模块的core包和dataSource包。core 包是JDBC的核心功能包,包括常用的JdbcTemplate类;dataSource包是访问数据源的工 具类包。使用Spring JDBC操作数据库,需要对其进行配置。配置文件的示例代码如下: <!-- 配置数据源 --> 扫一扫 视频讲解 <bean id="dataSource" class="org.springframework.jdbc.datasource. DriverManagerDataSource"> <!-- MySQL数据库驱动 --> <pro perty name="driverClassName" value="com.mysql.cj.jdbc. Driver"/> <!-- 连接数据库的URL --> <pro perty name="url" value="jdbc:mysql://127.0.0.1:3306/ springtest?useUnicode= true&characterEncoding=UTF-8&allowMultiQueries=true& serverTimezone=GMT%2B8"/> <!-- 连接数据库的用户名 --> <property name="username" value="root"/> <!-- 连接数据库的密码 --> <property name="password" value="root"/> </bean> <!-- 配置JDBC模板 --> <bea n id="jdbcTemplate" class="org.springframework.jdbc.core. JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> 在上述示例代码中,配置JDBC模板时,需要将dataSource注入jdbcTemplate,而在 数据访问层(Dao层)需要使用jdbcTemplate时,也需要将jdbcTemplate注入对应的Bean 中。示例代码如下: … @Repository("testDao") public class TestDaoImpl implements TestDao{ @Autowired //使用配置文件中的JDBC模板 private JdbcTemplate jdbcTemplate; … } XX5.1.2 Spring jdbcTemplate的使用方法 在获取JDBC模板后如何使用它是本节将要讲述的内容。首先需要了解jdbcTemplate 类的常用方法,该类的常用方法是update()和query()方法。 (1)public int update(String sql,Object args[]):该方法可以对数据表进行增加、修改、 删除等操作。使用args[]设置SQL语句中的参数,并返回更新的行数。示例代码如下: String insertSql = "insert into user values(null,?,?)"; Object param1[] = {"chenheng1", "男"}; jdbcTemplate.update(sql, param1); (2)public List<T> query (String sql, RowMapper<T> rowMapper, Object args[]):该方法 可以对数据表进行查询操作。rowMapper将结果集映射到用户自定义的类中(前提是自定 义类中的属性与数据表的字段对应)。示例代码如下: String selectSql ="select * from user"; RowMapper<MyUser> rowMapper = new BeanPropertyRowMapper<MyUser>(MyUser. class); List<MyUser> list = jdbcTemplate.query(sql, rowMapper, null); 下面通过一个实例演示Spring JDBC的使用过程。 【例5-1】 使用Spring JDBC访问数据库。 A. 创建模块并导入JAR包 创建名为ch5的项目,然后在ch5项目中创建一个名为ch5_1的模块,同时给ch5_1 模块添加Web Application,并将Spring的4个基础包、Spring Commons Logging Bridge 对应的JAR包spring-jcl-6.0.0.jar和spring-aop-6.0.0.jar、MySQL数据库的驱动程序JAR 包(mysql-connector-java-8.0.29.jar)、Spring JDBC的JAR包(spring-jdbc-6.0.0.jar)、Java 增强库(lombok-1.18.24.jar)以及Spring事务管理的JAR包(spring-tx-6.0.0.jar)复制到 ch5_1的WEB-INF/lib目录中,添加为模块依赖,如图5.1所示。 图5.1 ch5_1模块所依赖的JAR包 B. 创建并编辑配置文件 在ch5_1模块的src目录中创建配置文件applicationContext.xml,并在该文件中配置 数据源和JDBC模板,具体代码如下: <?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: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/context http ://www.springframework.org/schema/context/spring-context.xsd"> <!-- 指定需要扫描的包(包括子包),使注解生效 --> <context:component-scan base-package="dao"/> <!-- 配置数据源 --> <bea n id="dataSource" class="org.springframework.jdbc.datasource. DriverManagerDataSource"> <!-- MySQL数据库驱动 --> <pro perty name="driverClassName" value="com.mysql.cj.jdbc. Driver"/> <!-- 连接数据库的URL --> <pro perty name="url" value="jdbc:mysql://127.0.0.1:3306/springtest? useUnicode=true&characterEncoding=UTF-8& allowMultiQueries=true&serverTimezone=GMT%2B8"/> <!-- 连接数据库的用户名 --> <property name="username" value="root"/> <!-- 连接数据库的密码 --> <property name="password" value="root"/> </bean> <!-- 配置JDBC模板 --> <bea n id="jdbcTemplate" class="org.springframework.jdbc.core. JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> </beans> C. 创建实体类 在ch5_1模块的src目录中创建名为entity的包,并在该包中创建实体类MyUser。该 类的属性与数据表user中的字段一致。数据表user的结构如图5.2所示。 图5.2 数据表user的结构 实体类MyUser的代码如下: package entity; import lombok.Data; /** * 使用@Data注解实体属性无须Setter和Getter方法,但需要给IDEA安装Lombok 插件(安 * 装后重构模块) */ @Data public class MyUser { private Integer uid; private String uname; private String usex; public String toString() { retu rn "myUser [uid=" + uid +", uname=" + uname + ", usex=" + usex + "]"; } } D. 创建数据访问层Dao 在ch5_1模块的src目录中创建名为dao的包,并在dao包中创建TestDao接口和 TestDaoImpl实现类。在实现类TestDaoImpl中使用JDBC模板jdbcTemplate访问数据库, 并将该类注解为@Repository("testDao")。 TestDao接口的代码如下: package dao; import java.util.List; import entity.MyUser; public interface TestDao { public int update(String sql, Object[] param); public List<MyUser> query(String sql, Object[] param); } TestDaoImpl实现类的代码如下: package dao; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; import entity.MyUser; @Repository("testDao") public class TestDaoImpl implements TestDao{ @Autowired //使用配置文件中的JDBC模板 private JdbcTemplate jdbcTemplate; /** * 更新方法,包括添加、修改、删除 * param为sql中的参数,例如通配符? */ @Override public int update(String sql, Object[] param) { return jdbcTemplate.update(sql, param); } /** * 查询方法 * param为sql中的参数,例如通配符? */ @Override public List<MyUser> query(String sql, Object[] param) { RowM apper<MyUser> rowMapper = new BeanPropertyRowMapper <MyUser>(MyUser.class); return jdbcTemplate.query(sql, rowMapper, param); } } E. 创建测试类 在ch5_1模块的src目录中创建名为test的包,并在该包中创建测试类TestSpringJDBC。 在主方法中调用数据访问层Dao中的方法,对数据表user进行操作。具体代码如下: package test; import java.util.List; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import dao.TestDao; import entity.MyUser; public class TestSpringJDBC { private static ApplicationContext appCon; public static void main(String[] args) { appC on = new ClassPathXmlApplicationContext ("applicationContext.xml"); TestDao td = (TestDao)appCon.getBean("testDao"); String insertSql = "insert into user values(null,?,?)"; //数组param的值与insertSql语句中的?一一对应 Object param1[] = {"chenheng1", "男"}; Object param2[] = {"chenheng2", "女"}; Object param3[] = {"chenheng3", "男"}; Object param4[] = {"chenheng4", "女"}; //添加用户 td.update(insertSql, param1); td.update(insertSql, param2); td.update(insertSql, param3); td.update(insertSql, param4); //查询用户 String selectSql ="select * from user"; List<MyUser> list = td.query(selectSql, null); for(MyUser mu: list) { System.out.println(mu); } } } 运行上述测试类,运行结果如图5.3所示。 图5.3 Spring数据库编程的运行结果 5.2 编程式事务管理 在代码中显式调用beginTransaction()、commit()、rollback()等与事务管理相关的方法, 这就是编程式事务管理。当只有少数事务操作时,采用编程式事务管理比较合适。 XX5.2.1 基于底层API的编程式事务管理 基于底层API的编程式事务管理就是根据PlatformTransactionManager、TransactionDefinition 和TransactionStatus 3个核心接口通过编程的方式来进行事务管理。 下面通过一个实例讲解基于底层API的编程式事务管理。 【例5-2】 基于底层API的编程式事务管理。 A. 创建模块并导入JAR包 在ch5项目中创建一个名为ch5_2的模块,同时给ch5_2模块添加Web Application, 并将Spring的4个基础包、Spring Commons Logging Bridge对应的JAR包spring-jcl- 6.0.0.jar和spring-aop-6.0.0.jar、MySQL数据库的驱动程序JAR包(mysql-connector-java- 8.0.29.jar)、Spring JDBC的JAR包(spring-jdbc-6.0.0.jar)以及Spring事务管理的JAR 包(spring-tx-6.0.0.jar)复制到ch5_2的WEB-INF/lib目录中,添加为模块依赖。 B. 给数据源配置事务管理器 在ch5_2模块的src目录中创建配置文件applicationContext.xml,并在配置文件中使 用PlatformTransactionManager接口的一个间接实现类DataSourceTransactionManager为数 据源添加事务管理器,具体配置代码如下: <?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: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/context http: //www.springframework.org/schema/context/spring-context.xsd"> <!-- 指定需要扫描的包(包括子包),使注解生效 --> <context:component-scan base-package="dao"/> <!-- 配置数据源 --> <bean id="dataSource" class="org.springframework.jdbc.datasource. DriverManagerDataSource"> <!-- MySQL数据库驱动 --> <pro perty name="driverClassName" value="com.mysql.cj.jdbc. Driver"/> <!-- 连接数据库的URL --> 扫一扫 视频讲解 <pro perty name="url" value="jdbc:mysql://127.0.0.1:3306/ springtest?useUnicode=true&characterEncoding=UTF-8& allowMultiQueries=true&serverTimezone=GMT%2B8"/> <!-- 连接数据库的用户名 --> <property name="username" value="root"/> <!-- 连接数据库的密码 --> <property name="password" value="root"/> </bean> <!-- 配置JDBC模板 --> <bea n id="jdbcTemplate" class="org.springframework.jdbc.core. JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 为数据源添加事务管理器 --> <bean id="txManager" clas s="org.springframework.jdbc.datasource. DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> </beans> C. 创建数据访问类 在ch5_2模块的src目录中创建名为dao的包,在该包中创建数据访问类CodeTransaction, 并使用@Repository("codeTransaction")注解为数据访问层。在CodeTransaction类中使用编 程的方式进行数据库的事务管理。 CodeTransaction类的代码如下: package dao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.stereotype.Repository; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; @Repository("codeTransaction") public class CodeTransaction { @Autowired //使用配置文件中的JDBC模板 private JdbcTemplate jdbcTemplate; //依赖注入事务管理器txManager(配置文件中的Bean) @Autowired private DataSourceTransactionManager txManager; public void test() { //默认事务定义,例如隔离级别、传播行为等 TransactionDefinition tf = new DefaultTransactionDefinition(); //开启事务ts TransactionStatus ts = txManager.getTransaction(tf); //删除表中的数据 String sql = " delete from user "; //添加数据 String sql1 = " insert into user values(?,?,?) "; Object param[] = { 1, "陈恒", "男" }; try { //删除数据 jdbcTemplate.update(sql); //添加一条数据 jdbcTemplate.update(sql1, param); //添加相同的一条数据,使主键重复 jdbcTemplate.update(sql1, param); //提交事务 txManager.commit(ts); System.out.println("执行成功,没有事务回滚!"); } catch (Exception e) { System.out.println("主键重复,事务回滚!"); } } } D. 创建测试类 在ch5_2模块的src目录中创建名为test的包,并在该包中创建测试类TestCodeTransaction, 具体代码如下: package test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import dao.CodeTransaction; public class TestCodeTransaction { private static ApplicationContext appCon; public static void main(String[] args) { appC on = new ClassPathXmlApplicationContext ("applicationContext.xml"); Code Transaction ct = (CodeTransaction)appCon.getBean ("codeTransaction"); ct.test(); } } 上述测试类的运行结果如图5.4所示。 图5.4 基于底层API的编程式事务管理的测试结果 从程序运行前后数据表中的数据可以看出,取消了主键重复前执行的删除和插入操 作,即事务回滚。 XX5.2.2 基于TransactionTemplate的编程式事务管理 事务管理的代码散落在业务逻辑代码中,破坏了原有代码的条理性,并且每一个业务 方法都包含了类似的启动事务、提交以及回滚事务的样板代码。 TransactionTemplate的execute()方法有一个TransactionCallback接口类型的参数,该接 口中定义了一个doInTransaction()方法,通常以匿名内部类的方式实现TransactionCallback 接口,并在其doInTransaction()方法中书写业务逻辑代码。这里可以使用默认的事务提交 和回滚规则,在业务代码中不需要显式调用任何事务管理的API。doInTransaction()方法有 一个TransactionStatus类型的参数,可以在方法的任何位置调用该参数的setRollbackOnly() 方法将事务标识为回滚,以执行事务回滚。 根据默认规则,如果在执行回调方法的过程中抛出了未检查异常,或者显式调用了 setRollbackOnly()方法,则回滚事务;如果事务执行完成或者抛出了checked类型的异常, 则提交事务。 【例5-3】 基于TransactionTemplate的编程式事务管理。 A. 创建模块并导入JAR包 在ch5项目中创建一个名为ch5_3的模块,同时给ch5_3模块添加Web Application, 并将ch5_2模块的lib复制到ch5_3模块的WEB-INF目录中,添加为模块依赖。 B. 为事务管理器添加事务模板 在ch5_3模块的src目录中创建配置文件applicationContext.xml,在配置文件中使用 org.springframework.transaction.support.TransactionTemplate类为事务管理器添加事务模板, 具体配置代码如下: <?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: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/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 指定需要扫描的包(包括子包),使注解生效 --> <context:component-scan base-package="dao"/> <!-- 配置数据源 --> <bea n id="dataSource" class="org.springframework.jdbc.datasource. DriverManagerDataSource"> <!-- MySQL数据库驱动 --> <pro perty name="driverClassName" value="com.mysql.cj.jdbc. Driver"/> <!-- 连接数据库的URL --> <pro perty name="url" value="jdbc:mysql://127.0.0.1:3306/ springtest?useUnicode=true&characterEncoding=UTF-8& allowMultiQueries=true&serverTimezone=GMT%2B8"/> <!-- 连接数据库的用户名 --> <property name="username" value="root"/> <!-- 连接数据库的密码 --> <property name="password" value="root"/> </bean> <!-- 配置JDBC模板 --> <bea n id="jdbcTemplate" class="org.springframework.jdbc.core. JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 为数据源添加事务管理器 --> <bean id="txManager" clas s="org.springframework.jdbc.datasource. DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 为事务管理器txManager创建transactionTemplate --> <bea n id="transactionTemplate" class="org.springframework. transaction.support.TransactionTemplate"> <property name="transactionManager" ref="txManager"/> </bean> </beans> C. 创建数据访问类 在ch5_3模块的src目录中创建名为dao的包,并在该包中创建数据访问类 TransactionTemplateDao,同时注解为@Repository("transactionTemplateDao")。在 TransactionTemplateDao类中使用编程的方式进行数据库的事务管理。 数据访问类TransactionTemplateDao的代码具体如下: package dao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; @Repository("transactionTemplateDao") public class TransactionTemplateDao { //依赖注入JDBC模板 @Autowired private JdbcTemplate jdbcTemplate; //依赖注入transactionTemplate @Autowired private TransactionTemplate transactionTemplate; public void test() { //以匿名内部类的方式实现TransactionCallback接口,使用默认的事务提交和回滚 //规则,在业务代码中不需要显式调用任何事务处理的API transactionTemplate.execute(new TransactionCallback<Object>(){ @Override public Object doInTransaction(TransactionStatus arg0) { //删除表中的数据 String sql = " delete from user "; //添加数据 String sql1 = " insert into user values(?,?,?) "; Object param[] = {1,"陈恒","男"}; try{ //删除数据 jdbcTemplate.update(sql); //添加一条数据 jdbcTemplate.update(sql1, param); //添加相同的一条数据,使主键重复 jdbcTemplate.update(sql1, param); System.out.println("执行成功,没有事务回滚!"); }catch(Exception e){ System.out.println("主键重复,事务回滚!"); } return null; } }); } } D. 创建测试类 在ch5_3模块的src目录中创建名为test的包,并在该包中创建测试类 TransactionTemplateTest,测试类的代码具体如下: package test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import dao.TransactionTemplateDao; public class TransactionTemplateTest { private static ApplicationContext appCon; public static void main(String[] args) { appC on = new ClassPathXmlApplicationContext ("applicationContext.xml"); Tran sactionTemplateDao ct = (TransactionTemplateDao)appCon. getBean("transactionTemplateDao"); ct.test(); } } 5.3 声明式事务管理 Spring的声明式事务管理是通过AOP技术实现的事务管理,其本质是对方法执行前 后进行拦截,在目标方法执行前,创建或者加入一个事务;在目标方法执行后,根据执行 情况提交事务或回滚事务。 声明式事务管理最大的优点是不需要通过编程的方式管理事务,因此不需要在业务逻 辑代码中掺杂事务管理的代码,只需要相关的事务规则声明,便可以将事务规则应用到业 务逻辑中。通常情况下,在开发中使用声明式事务管理,不仅因为其简单,更因为这样使 纯业务代码不被污染,极大地方便了后期的代码维护。 与编程式事务管理相比,声明式事务管理唯一不足的地方是,最细粒度只能作用到方 法级别,无法像编程式事务管理那样可以作用到代码块级别。如果的确有需求,也可以通 过变通的方法进行解决,比如将需要进行事务管理的代码块独立为方法。 Spring的声明式事务管理可以通过两种方式来实现:一种是基于XML的方式;另一 种是基于@Transactional注解的方式。 XX5.3.1 基于XML方式的声明式事务管理 基于XML方式的声明式事务管理是通过在配置文件中配置事务规则的相关声明来 实现的。Spring框架提供了tx命名空间来配置事务,用<tx:advice>元素来配置事务的通 知。在配置<tx:advice>元素时,一般需要指定id和transaction-manager属性,其中id属 性是配置文件中的唯一标识,transaction-manager属性用于指定事务管理器。另外还需要 <tx:attributes>子元素,该子元素可配置多个<tx:method>子元素指定执行事务的细节。 当<tx:advice>元素配置了事务的增强处理后,就可以通过编写AOP配置,让Spring 自动对目标对象生成代理。下面通过一个实例演示如何通过XML方式来实现Spring的声 明式事务管理。 【例5-4】 基于XML方式的声明式事务管理。 为了体现事务管理的流程,本实例创建了Dao、Service和Controller 3层,具体实现 步骤如下。 A. 创建模块并导入JAR包 在ch5项目中创建一个名为ch5_4的模块,同时给ch5_4模块添加Web Application, 并将如图5.5所示的JAR包复制到ch5_4的WEB-INF/lib目录中,添加为模块依赖。 图5.5 ch5_4模块所依赖的JAR包 扫一扫 视频讲解