笔记来源:尚硅谷Spring框架视频教程(spring5源码级讲解)

JdbcTemplate与声明式事务

1、JdbcTemplate

1.1、概述

前面我们已经学习了 Spring 中的Core Container核心部分和AOPAspects等面向切面编程部分,接下来就是Data Access/Integration即数据访问和集成部分

Spring 既可以单独使用,也可以集成其他框架,如HibernateMyBatis等。除此之外,其中对于JDBC也做了封装,即本章节的JdbcTemplate,用它可以比较方便地对数据库进行增删改查等操作

03-JdbcTemplate与声明式事务 - 图1

总结一下:

  • JdbcTemplate就是 Spring 框架对JDBC技术进行的二次封装模板,能够简化对数据库的操作

1.2、准备工作

步骤预览

  • 1)引入相关jar
  • 2)Spring 配置文件配置Druid连接池信息
  • 3)配置JdbcTemplate对象,注入dataSource
  • 4)创建 Service 和 Dao 类,在 Dao 类中注入JdbcTemplate对象

详细操作

  • 1)引入相关jar包(或依赖)

    • druid
    • mysql-connector-java
    • spring-jdbc
    • spring-orm
    • spring-tx

03-JdbcTemplate与声明式事务 - 图2

  • 2)Spring 配置文件配置Druid连接池信息
  1. <context:property-placeholder location="classpath:jdbc.properties"/>
  2. <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
  3. <property name="driverClassName" value="${mysql.driverClassName}"/>
  4. <property name="url" value="${mysql.url}"/>
  5. <property name="username" value="${mysql.username}"/>
  6. <property name="password" value="${mysql.password}"/>
  7. </bean>

沿用之前章节的Jdbc.properties配置信息,但稍作修改

  1. mysql.driverClassName=com.mysql.jdbc.Driver
  2. mysql.url=jdbc:mysql:///book_db
  3. mysql.username=root
  4. mysql.password=root
  • 3)配置JdbcTemplate对象,注入dataSource
  1. <!--配置JdbcTemplate-->
  2. <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
  3. <!--属性注入dataSource-->
  4. <property name="dataSource" ref="dataSource"></property>
  5. </bean>

为何使用属性注入?

JdbcTemplate虽然含有DataSource的有参构造,但其调用了setDataSource()方法

03-JdbcTemplate与声明式事务 - 图3

这个方法是在其父类中定义了的

03-JdbcTemplate与声明式事务 - 图4

  • 4)创建 Service 和 Dao 类,在 Dao 类中注入JdbcTemplate对象

Dao 类

  1. public interface BookDao {
  2. }
  3. @Repository
  4. public class BookDaoImpl implements BookDao {
  5. @Autowired
  6. private JdbcTemplate jdbcTemplate;
  7. }

Service 类

  1. @Service
  2. public class BookService {
  3. @Autowired
  4. private BookDao bookDao;
  5. }

别忘了开启注解扫描

  1. <!--开启注解扫描-->
  2. <context:component-scan base-package="com.vectorx.spring5.s15_jdbctemplate"/>

配置文件整体结构

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  6. http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
  7. <!--开启注解扫描-->
  8. <context:component-scan base-package="com.vectorx.spring5.s15_jdbctemplate"/>
  9. <!--配置dataSource-->
  10. <context:property-placeholder location="classpath:jdbc.properties"/>
  11. <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
  12. <property name="driverClassName" value="${mysql.driverClassName}"/>
  13. <property name="url" value="${mysql.url}"/>
  14. <property name="username" value="${mysql.username}"/>
  15. <property name="password" value="${mysql.password}"/>
  16. </bean>
  17. <!--配置JdbcTemplate-->
  18. <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
  19. <!--属性注入dataSource-->
  20. <property name="dataSource" ref="dataSource"></property>
  21. </bean>
  22. </beans>

1.3、添加操作

步骤预览

  • 1)创建数据库中t_book表对应的实体对象
  • 2)编写 Service 和 Dao 代码,增加添加图书的功能逻辑
  • 3)代码测试

详细操作

  • 1)创建数据库中t_book表对应的实体对象
  1. public class Book {
  2. private String bid;
  3. private String bname;
  4. private String bstatus;
  5. public String getBid() {
  6. return bid;
  7. }
  8. public void setBid(String bid) {
  9. this.bid = bid;
  10. }
  11. public String getBname() {
  12. return bname;
  13. }
  14. public void setBname(String bname) {
  15. this.bname = bname;
  16. }
  17. public String getBstatus() {
  18. return bstatus;
  19. }
  20. public void setBstatus(String bstatus) {
  21. this.bstatus = bstatus;
  22. }
  23. }
  • 2)编写 Service 和 Dao 代码,增加添加图书的功能逻辑

Service 类:添加addBook()方法

  1. @Service
  2. public class BookService {
  3. @Autowired
  4. private BookDao bookDao;
  5. public int addBook(Book book) {
  6. return bookDao.add(book);
  7. }
  8. }

Dao 类:通过操作JdbcTemplate对象的update()方法可实现插入,其中两个参数分别是

  • 第一个参数sql:编写插入数据对应的sql语句,可使用通配符?做占位符
  • 第二个参数args:可变参数列表,设置占位符对应的参数值
  1. public interface BookDao {
  2. int add(Book book);
  3. }
  4. @Repository
  5. public class BookDaoImpl implements BookDao {
  6. @Autowired
  7. private JdbcTemplate jdbcTemplate;
  8. @Override
  9. public int add(Book book) {
  10. //操作JdbcTemplate对象,使用update方法进行添加操作
  11. String sql = "insert into t_book(bid,bname,bstatus) values(?,?,?)";
  12. Object[] args = {book.getBid(), book.getBname(), book.getBstatus()};
  13. return jdbcTemplate.update(sql, args);
  14. }
  15. }
  • 3)代码测试
  1. ApplicationContext context = new ClassPathXmlApplicationContext("bean13.xml");
  2. BookService bookService = context.getBean("bookService", BookService.class);
  3. //模拟新增图书
  4. Book book = new Book();
  5. book.setBid("1");
  6. book.setBname("Spring JdbcTemplate");
  7. book.setBstatus("1");
  8. int result = bookService.addBook(book);
  9. System.out.println(result);

测试结果

  1. Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
  2. 三月 06, 2022 10:25:49 下午 com.alibaba.druid.pool.DruidDataSource info
  3. 信息: {dataSource-1} inited
  4. 1

刷新数据库中t_book表数据,核验是否插入成功

03-JdbcTemplate与声明式事务 - 图5

可以看到,表中成功新增了一条数据

1.4、修改和删除

修改、删除操作和添加操作代码逻辑基本一致

BookService 类:添加updateBook()deleteBook()方法

  1. // 修改
  2. public int updateBook(Book book) {
  3. return bookDao.update(book);
  4. }
  5. //删除
  6. public int deleteBook(String id) {
  7. return bookDao.delete(id);
  8. }

BookDao 类:添加update()delete()方法

  1. // 修改
  2. int update(Book book);
  3. // 删除
  4. int delete(String id);

BookDaoImpl 类:实现update()delete()方法

  1. // 修改
  2. @Override
  3. public int update(Book book) {
  4. String sql = "update t_book set bname=?,bstatus=? where bid=?";
  5. Object[] args = {book.getBname(), book.getBstatus(), book.getBid()};
  6. return jdbcTemplate.update(sql, args);
  7. }
  8. // 删除
  9. @Override
  10. public int delete(String id) {
  11. String sql = "delete from t_book where bid=? ";
  12. return jdbcTemplate.update(sql, id);
  13. }

测试修改

  1. //修改图书信息
  2. Book book = new Book();
  3. book.setBid("1");
  4. book.setBname("JdbcTemplate");
  5. book.setBstatus("update");
  6. int result2 = bookService.updateBook(book);
  7. System.out.println(result2);

测试结果

03-JdbcTemplate与声明式事务 - 图6

测试删除

  1. //删除图书
  2. int result3 = bookService.deleteBook("1");
  3. System.out.println(result3);

测试结果

03-JdbcTemplate与声明式事务 - 图7

1.5、查询操作

这里演示三种查询操作:

  • 1)查询返回某个值
  • 2)查询返回对象
  • 3)查询返回集合

为了演示效果,需要先在数据库的t_book表中添加两条数据

03-JdbcTemplate与声明式事务 - 图8

接着我们先将代码完成,最后再作进一步的分析说明

代码实现

BookService 类:添加findCount()findById()findAll()方法

  1. // 查找返回一个值
  2. public int findCount() {
  3. return bookDao.selectCount();
  4. }
  5. // 查找返回对象
  6. public Book findById(String id) {
  7. return bookDao.selectById(id);
  8. }
  9. // 查找返回集合
  10. public List<Book> findAll() {
  11. return bookDao.selectAll();
  12. }

BookDao 类:添加selectCount()selectById()selectAll()方法

  1. // 查找返回一个值
  2. int selectCount();
  3. // 查找返回对象
  4. Book selectById(String id);
  5. // 查找返回集合
  6. List<Book> selectAll();

BookDaoImpl 类:实现selectCount()selectById()selectAll()方法

  1. // 查找返回一个值
  2. @Override
  3. public int selectCount() {
  4. String sql = "select count(0) from t_book";
  5. return jdbcTemplate.queryForObject(sql, Integer.class);
  6. }
  7. // 查找返回对象
  8. @Override
  9. public Book selectById(String id) {
  10. String sql = "select * from t_book where bid=?";
  11. return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);
  12. }
  13. // 查找返回集合
  14. @Override
  15. public List<Book> selectAll() {
  16. String sql = "select * from t_book where 1=1";
  17. return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Book.class));
  18. }

测试代码

  1. int count = bookService.findCount();
  2. System.out.println(count);
  3. Book book = bookService.findById("1");
  4. System.out.println(book);
  5. List<Book> bookList = bookService.findAll();
  6. System.out.println(bookList);

测试结果

  1. 2
  2. Book{bid='1', bname='Spring', bstatus='add'}
  3. [Book{bid='1', bname='Spring', bstatus='add'}, Book{bid='2', bname='SpringMVC', bstatus='add'}]

代码分析

上述代码逻辑中使用到了queryForObject()query()方法

  1. jdbcTemplate.queryForObject(sql, Integer.class);
  2. jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), id);
  3. jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Book.class));

分别对应JdbcTemplate中的三个方法

  1. public <T> T queryForObject(String sql, Class<T> requiredType);
  2. public <T> T queryForObject(String sql, RowMapper<T> rowMapper, Object... args);
  3. public <T> List<T> query(String sql, RowMapper<T> rowMapper);

其中,有两个参数值得关注,一个是Class<T> requiredType,另一个是RowMapper<T> rowMapper

  • Class<T> requiredType:返回值的Class类型
  • RowMapper<T> rowMapper:是一个接口,返回不同类型数据,可以使用其实现类进行数据的封装。其实现类有很多,因为我们需要返回一个数据库实体对象,所以可以选择使用BeanPropertyRowMapper

03-JdbcTemplate与声明式事务 - 图9

另外,queryForObject(String sql, RowMapper<T> rowMapper, Object... args)query(String sql, RowMapper<T> rowMapper)

区别在于

  • queryForObject返回一个对象
  • query返回一个集合

1.6、批量操作

JdbcTemplate中提供了batchUpdate()可供我们进行批量操作,如:批量添加、批量修改、批量删除等,代码实现上大同小异,我们对代码进行快速实现

代码实现

BookService 类:添加batchAddBook()batchUpdateBook()batchDelBook()方法

  1. // 批量添加
  2. public void batchAddBook(List<Object[]> bookList) {
  3. bookDao.batchAdd(bookList);
  4. }
  5. // 批量修改
  6. public void batchUpdateBook(List<Object[]> bookList) {
  7. bookDao.batchUpdate(bookList);
  8. }
  9. // 批量删除
  10. public void batchDelBook(List<Object[]> bookList) {
  11. bookDao.batchDel(bookList);
  12. }

BookDao 类:添加batchAdd()batchUpdate()batchDel()方法

  1. // 批量添加
  2. void batchAdd(List<Object[]> bookList);
  3. // 批量修改
  4. void batchUpdate(List<Object[]> bookList);
  5. // 批量删除
  6. void batchDel(List<Object[]> bookList);

BookDaoImpl 类:实现batchAdd()batchUpdate()batchDel()方法

  1. // 批量添加
  2. @Override
  3. public void batchAdd(List<Object[]> bookList) {
  4. String sql = "insert into t_book(bid,bname,bstatus) values(?,?,?)";
  5. extractBatch(sql, bookList);
  6. }
  7. // 批量修改
  8. @Override
  9. public void batchUpdate(List<Object[]> bookList) {
  10. String sql = "update t_book set bname=?,bstatus=? where bid=?";
  11. extractBatch(sql, bookList);
  12. }
  13. // 批量删除
  14. @Override
  15. public void batchDel(List<Object[]> bookList) {
  16. String sql = "delete from t_book where bid=? ";
  17. extractBatch(sql, bookList);
  18. }
  19. private void extractBatch(String sql, List<Object[]> bookList,) {
  20. int[] ints = jdbcTemplate.batchUpdate(sql, bookList);
  21. System.out.println(ints);
  22. }

代码测试

测试批量添加

  1. // 批量添加
  2. List<Object[]> bookList = new ArrayList<>();
  3. Object[] book1 = {"3", "Java", "batchAdd"};
  4. Object[] book2 = {"4", "Python", "batchAdd"};
  5. Object[] book3 = {"5", "C#", "batchAdd"};
  6. bookList.add(book1);
  7. bookList.add(book2);
  8. bookList.add(book3);
  9. bookService.batchAddBook(bookList);

测试结果

03-JdbcTemplate与声明式事务 - 图10

测试批量修改

  1. // 批量修改
  2. List<Object[]> bookList = new ArrayList<>();
  3. Object[] book1 = {"Java++", "batchUpdate", "3"};
  4. Object[] book2 = {"Python++", "batchUpdate", "4"};
  5. Object[] book3 = {"C#++", "batchUpdate", "5"};
  6. bookList.add(book1);
  7. bookList.add(book2);
  8. bookList.add(book3);
  9. bookService.batchUpdateBook(bookList);

测试结果

03-JdbcTemplate与声明式事务 - 图11

测试批量删除

  1. // 批量删除
  2. List<Object[]> bookList = new ArrayList<>();
  3. Object[] book1 = {"3"};
  4. Object[] book2 = {"4"};
  5. bookList.add(book1);
  6. bookList.add(book2);
  7. bookService.batchDelBook(bookList);

测试结果

03-JdbcTemplate与声明式事务 - 图12

可以看出,上述测试都完全符合我们的预期

03-JdbcTemplate与声明式事务 - 图13

小结

简单总结下JdbcTemplate操作数据库的各个方法

  • 添加、修改、删除操作:update()方法
  • 查询操作:queryForObject()query()方法,关注两个参数:

    • Class<T> requiredType:返回值的Class类型
    • RowMapper<T> rowMapper:接口,具体实现类BeanPropertyRowMapper,封装对象实体
  • 批量操作:batchUpdate()方法

2、事务

2.1、事务概念

  • 1)事务是数据库操作的最基本单元,是逻辑上的一组操作。这一组操作,要么都成功,要么都失败(只要有一个操作失败,所有操作都失败)
  • 2)典型场景:银行转账。Lucy 转账 100 元给 Mary,Lucy 少 100,Mary 多 100。转账过程中若出现任何问题,双方都不会多钱或少钱,转账就不会成功

2.2、事务四个特性(ACID)

  • 原子性(Atomicity):一个事务中的所有操作,要么都成功,要么都失败,整个过程不可分割
  • 一致性(Consistency):事务操作之前和操作之后,总量保持不变
  • 隔离性(Isolation):多事务操作时,相互之间不会产生影响
  • 持久性(Durability):事务最终提交后,数据库表中数据才会真正发生变化

2.3、搭建事务操作环境

我们知道 JavaEE 中的三层架构分为:表示层(web层)、业务逻辑层(service层)、数据访问层(dao层)

  • web层:与客户端进行交互
  • service层:处理业务逻辑
  • dao层:与数据库进行交互

因此,我们搭建操作环境也按照典型的三层架构来实现,不过目前现阶段我们只关注ServiceDao两层

03-JdbcTemplate与声明式事务 - 图14

我们以银行转账为例,因为整个转账操作包括两个操作:出账的操作和入账的操作

过程概览

  • 1)创建数据库表结构,添加几条记录
  • 2)创建ServiceDao类,完成对象创建和关系注入
  • 3)Dao中创建两个方法:出账的方法、入账的方法;Service中创建转账的方法

过程详解

1)创建数据库表结构,添加几条记录

  1. # 建表语句
  2. create table t_account
  3. (
  4. id varchar(20) not null,
  5. username varchar(50) null,
  6. amount int null,
  7. constraint transfer_record_pk
  8. primary key (id)
  9. );
  10. # 添加语句
  11. INSERT INTO book_db.t_account (id, username, amount) VALUES ('1', 'Lucy', 1000);
  12. INSERT INTO book_db.t_account (id, username, amount) VALUES ('2', 'Mary', 1000);

添加完成效果

03-JdbcTemplate与声明式事务 - 图15

2)创建ServiceDao类,完成对象创建和关系注入

Service中注入DaoDao中注入JdbcTemplateJdbcTemplate中注入DataSource

ServiceDao

  1. public interface TransferRecordDao {
  2. }
  3. @Repository
  4. public class TransferRecordDaoImpl implements TransferRecordDao {
  5. @Autowired
  6. private JdbcTemplate jdbcTemplate;
  7. }
  8. @Service
  9. public class TransferRecordService {
  10. @Autowired
  11. private TransferRecordDao transferRecordDao;
  12. }

Spring 配置文件

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  6. http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
  7. <!--开启注解扫描-->
  8. <context:component-scan base-package="com.vectorx.spring5.s16_transaction"/>
  9. <!--配置dataSource-->
  10. <context:property-placeholder location="classpath:jdbc.properties"/>
  11. <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
  12. <property name="driverClassName" value="${mysql.driverClassName}"/>
  13. <property name="url" value="${mysql.url}"/>
  14. <property name="username" value="${mysql.username}"/>
  15. <property name="password" value="${mysql.password}"/>
  16. </bean>
  17. <!--配置JdbcTemplate-->
  18. <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
  19. <!--属性注入dataSource-->
  20. <property name="dataSource" ref="dataSource"></property>
  21. </bean>
  22. </beans>

3)Dao中创建两个方法:出账的方法、入账的方法;Service中创建转账的方法

  • Dao负责数据库操作,所以需要创建两个方法:出账的方法、入账的方法
  1. public interface TransferRecordDao {
  2. void transferOut(int amount, String username);
  3. void transferIn(int amount, String username);
  4. }
  5. @Repository
  6. public class TransferRecordDaoImpl implements TransferRecordDao {
  7. @Autowired
  8. private JdbcTemplate jdbcTemplate;
  9. @Override
  10. public void transferOut(int amount, String username) {
  11. String sql = "update t_account set amount=amount-? where username=?";
  12. Object[] args = {amount, username};
  13. jdbcTemplate.update(sql, args);
  14. }
  15. @Override
  16. public void transferIn(int amount, String username) {
  17. String sql = "update t_account set amount=amount+? where username=?";
  18. Object[] args = {amount, username};
  19. jdbcTemplate.update(sql, args);
  20. }
  21. }
  • Service负责业务操作,所以需要创建一个方法,来调用Dao中两个方法
  1. @Service
  2. public class TransferRecordService {
  3. @Autowired
  4. private TransferRecordDao transferRecordDao;
  5. public void transferAccounts(int amount, String fromUser, String toUser) {
  6. transferRecordDao.transferOut(amount, fromUser);
  7. transferRecordDao.transferIn(amount, toUser);
  8. }
  9. }

测试代码

  1. ApplicationContext context = new ClassPathXmlApplicationContext("bean14.xml");
  2. TransferRecordService transferRecordService = context.getBean("transferRecordService", TransferRecordService.class);
  3. transferRecordService.transferAccounts(100, "Lucy", "Mary");

测试结果

03-JdbcTemplate与声明式事务 - 图16

可以发现,转账如期完成了。但真的没有一点问题么?

2.4、引入事务场景

我们模拟下在转账中途发生网络异常,修改TransferRecordService中转账方法

  1. public void transferAccounts(int amount, String fromUser, String toUser) {
  2. transferRecordDao.transferOut(amount, fromUser);
  3. //模拟网络异常而导致操作中断
  4. int i = 10 / 0;
  5. transferRecordDao.transferIn(amount, toUser);
  6. }

为了更清晰直观地看到数据的变化,我们还原数据表数据到最初状态

03-JdbcTemplate与声明式事务 - 图17

按照期望,转账应该失败,即双方账户不应该有任何变化。事实真的能够如我们所料么?

我们执行测试方法,如期抛出异常

  1. Exception in thread "main" java.lang.ArithmeticException: / by zero
  2. at com.vectorx.spring5.s16_transaction.service.TransferRecordService.transferAccounts(TransferRecordService.java:15)
  3. at com.vectorx.spring5.s16_transaction.TestTransfer.main(TestTransfer.java:11)

那数据表是否也如期变化呢?

03-JdbcTemplate与声明式事务 - 图18

我们发现,Lucy虽然成功转出了 100 元,但Mary没有成功到账 100 元。从现实的角度来说,这个问题很严重!!!

从事务的角度来说,这个转账操作没有遵循事务的原子性、一致性,即没有做到“要么都成功,要么都失败”,也没有做到“操作前后的总量不变”

综上所述,我们需要引入事务

2.5、事务基本操作

事务的基本操作过程如下

  • Step1、开启一个事务
  • Step2、进行业务逻辑实现
  • Step3、没有异常,则提交事务
  • Step4、发生异常,则回滚事务

事务的一般实现如下

  1. try {
  2. // Step1、开启一个事务
  3. // Step2、进行业务逻辑实现
  4. transferRecordDao.transferOut(amount, fromUser);
  5. //模拟网络异常而导致操作中断
  6. int i = 10 / 0;
  7. transferRecordDao.transferIn(amount, toUser);
  8. // Step3、没有异常,则提交事务
  9. } catch (Exception e) {
  10. // Step4、发生异常,则回滚事务
  11. }

不过,在 Spring 框架中提供了更方便的方式实现事务。“欲知后事如何,且听下回分解”

小结

本小结主要内容关键点

  • 事务的基本概念:数据库操作的基本单元,逻辑上的一组操作,要么都成功,要么都失败
  • 事务的四个基本特性:ACID,即原子性、一致性、隔离性和持久性

3、声明式事务

3.1、Spring事务管理

事务一般添加到三层结构中的Service层(业务逻辑层)

在 Spring 中进行事务管理操作有两种方式:编程式事务管理和声明式事务管理

  • 编程式事务管理(不推荐):上述事务的一般实现就是典型的编程式事务管理实现。但这种方式虽然并不好,但仍然需要我们有一定的了解,知道有这么一个过程即可。一般不推荐使用这种方式,主要原因如下

    • 1)实现不方便
    • 2)造成代码臃肿
    • 3)维护起来麻烦
  • 声明式事务管理(推荐使用):底层原理就是AOP,就是在不改变原代码基础上,扩展代码功能。有两种实现方式

    • 基于注解方式(推荐方式)
    • 基于XML配置文件方式

3.2、Spring事务管理API

提供了一个接口,代表事务管理器,针对不同框架存在不同实现类

03-JdbcTemplate与声明式事务 - 图19

主要掌握

  • PlatformTransactionManager接口:即事务管理器,有多个不同的抽象类和具体实现类,可满足不同的框架
  • DataSourceTrasactionManager实现类:JdbcTemplateMyBatis框架使用到它
  • HibernateTransactionManager实现类:Hibernate框架使用到它

3.3、声明式事务(注解方式)

  • 1)在 Spring 配置文件中配置事务管理器:配置DataSourceTransactionManager对象创建
  1. <!--配置事务管理器-->
  2. <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  3. <property name="dataSource" ref="dataSource"></property>
  4. </bean>
  • 2)在 Spring 配置文件中开启事务:引入xmlns:tx的名称空间,并配置<tx:annotation-driven>标签以开启事务注解
  1. <beans xmlns="http://www.springframework.org/schema/beans"
  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xmlns:context="http://www.springframework.org/schema/context"
  4. xmlns:tx="http://www.springframework.org/schema/tx"
  5. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  6. http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
  7. http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
  8. <!--其他配置信息略-->
  9. <!--开启事务注解-->
  10. <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
  11. </beans>
  • 3)在Service(或Service的方法)上添加事务注解@Transactional
  1. @Service
  2. @Transactional
  3. public class TransferRecordService {
  4. //...
  5. }

首先还原数据表信息至初始状态

03-JdbcTemplate与声明式事务 - 图20

测试代码后刷新数据表

03-JdbcTemplate与声明式事务 - 图21

这一次数据没有发生变化,说明事务回滚了,符合我们预期

3.4、事务参数

Service类上面添加注解@Transactional,在这个注解里面可以配置事务相关参数

03-JdbcTemplate与声明式事务 - 图22

主要介绍参数有

  • propagation:事务传播行为
  • isolation:事务隔离级别
  • timeout:超时时间
  • readOnly:是否只读
  • rollbackFor:回滚
  • noRollbackFor:不回滚

3.5、传播行为

  • 事务传播行为:多事务方法直接进行调用,这个过程中事务是如何进行管理的
  • 事务方法:让数据表数据发生变化的操作

事务的传播行为可以有传播属性指定。Spring 框架中定义了 7 种类传播行为:

传播属性 描述
REQUIRED 如果有事务在运行,当前方法就在此事务内运行;否则,就启动一个新的事务,并在自己的事务内运行
REQUIRED_NEW 当前方法必须启动新事务,并在它自己的事务内运行;如果有事务正在运行,应该将它挂起
SUPPORTS 如果有事务在运行,当前方法就在此事务内运行;否则,它可以不运行在事务中
NOT_SOPPORTED 当前方法不应该运行在事务内部,如果有运行的事务,就将它挂起
MANDATORY 当前方法必须运行在事务内部,如果没有正在运行的事务,就抛出异常
NEVER 当前方法不应该运行在事务中,如果有运行的事务,就抛出异常
NESTED 如果有事务在运行,当前方法就应该在此事务的嵌套事务内运行;否则,就启动一个新的事务,并在它自己的事务内运行

举个例子:定义两个方法add()update()

  1. @Transactional
  2. public void add(){
  3. // 调用update方法
  4. update();
  5. }
  6. public void update(){
  7. // do something
  8. }

则按照不同的传播属性,可以有以下解释

  • REQUIRED

    • 如果add()方法本身有事务,调用update()方法之后,update()使用当前add()方法里面事务;
    • 如果add()方法本身没有事务,调用update()方法之后,创建新的事务
  • REQUIRED_NEW

    • 使用add()方法调用update()方法,无论add()是否有事务,都创建新的事务

代码实现

  1. @Service
  2. @Transactional(propagation = Propagation.REQUIRED)
  3. public class TransferRecordService {
  4. //...
  5. }

等价于

  1. @Service
  2. @Transactional
  3. public class TransferRecordService {
  4. //...
  5. }

即默认不写,其事务传播行为就是使用的REQUIRED

3.6、隔离级别

在事务的四个特性中,隔离性(isolation)指的是多事务之间互不影响。不考虑隔离性,在并发时会产生一系列问题

有三个典型的“读”的问题:脏读、不可重复读、虚(幻)读

  • 脏读:一个未提交事务读取到了另一个未提交事务修改的数据

03-JdbcTemplate与声明式事务 - 图23

  • 不可重复读:一个未提交事务读取到了另一个已提交事务修改的数据(不能算问题,只是算现象)

03-JdbcTemplate与声明式事务 - 图24

  • 虚(幻)读:一个未提交事务读取到了另一个已提交事务添加的数据

通过设置隔离级别,可以解决上述“读”的问题

事务隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED(读未提交)
READ COMMITTED(读已提交) ×
REPEATABLE READ(可重复读) × ×
SERIALIZABLE(串行化) × × ×

代码实现

  1. @Service
  2. @Transactional(isolation = Isolation.REPEATABLE_READ)
  3. public class TransferRecordService {
  4. //...
  5. }

小课堂:MySQL 中默认事务隔离级别为REPEATABLE READ(可重复读)

3.7、其他参数

  • timeout:设置事务超时时间。事务需要在一定的时间内进行提交,若设定时间内事务未完成提交,则对事务进行回滚。默认值为-1,设置时间以秒为单位
  1. @Service
  2. @Transactional(timeout = 5)
  3. public class TransferRecordService {
  4. @Autowired
  5. private TransferRecordDao transferRecordDao;
  6. public void transferAccounts(int amount, String fromUser, String toUser) {
  7. transferRecordDao.transferOut(amount, fromUser);
  8. //模拟处理超时
  9. try {
  10. Thread.sleep(6000);
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. transferRecordDao.transferIn(amount, toUser);
  15. }
  16. }

设置超时时间后,执行测试代码,则日志信息会抛出TransactionTimedOutException事务超时异常

  1. Exception in thread "main" org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Wed Mar 09 21:30:33 CST 2022
  2. at org.springframework.transaction.support.ResourceHolderSupport.checkTransactionTimeout(ResourceHolderSupport.java:155)
  3. at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInMillis(ResourceHolderSupport.java:144)
  4. at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInSeconds(ResourceHolderSupport.java:128)
  5. at org.springframework.jdbc.datasource.DataSourceUtils.applyTimeout(DataSourceUtils.java:341)
  6. ...
  • readOnly:是否只读。

    • 默认值为false,表示读写操作都允许,可以进行增、删、改、查等操作;
    • 可设置为true,表示只允许读操作,即只能进行查询操作
  1. @Service
  2. @Transactional(readOnly = true)
  3. public class TransferRecordService {
  4. //...
  5. }

设置只读后,执行测试代码,则日志信息会抛出TransientDataAccessResourceException瞬态数据访问资源异常,同时还会抛出SQLException,并指出“连接是只读的,查询导致数据修改是不允许的”

  1. Exception in thread "main" org.springframework.dao.TransientDataAccessResourceException: PreparedStatementCallback; SQL [update t_account set amount=amount-? where username=?]; Connection is read-only. Queries leading to data modification are not allowed; nested exception is java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
  2. ...
  3. Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
  4. ...
  • rollbackFor:设置出现哪些异常才进行回滚
  1. @Service
  2. @Transactional(rollbackFor = ArithmeticException.class)
  3. public class TransferRecordService {
  4. @Autowired
  5. private TransferRecordDao transferRecordDao;
  6. public void transferAccounts(int amount, String fromUser, String toUser) {
  7. transferRecordDao.transferOut(amount, fromUser);
  8. //模拟网络异常而导致操作中断
  9. int i = 10 / 0;
  10. transferRecordDao.transferIn(amount, toUser);
  11. }
  12. }

上述代码表明,只有在抛出的异常为ArithmeticException时,才会进行事务的回滚操作

此时运行测试代码,后台会抛出ArithmeticException异常,因此会进行回滚,转账过程不会成功

此时数据库中的数据,就不会有任何变化

03-JdbcTemplate与声明式事务 - 图25

  • noRollbackFor:设置出现哪些异常不进行回滚
  1. @Service
  2. @Transactional(noRollbackFor = ArithmeticException.class)
  3. public class TransferRecordService {
  4. @Autowired
  5. private TransferRecordDao transferRecordDao;
  6. public void transferAccounts(int amount, String fromUser, String toUser) {
  7. transferRecordDao.transferOut(amount, fromUser);
  8. //模拟网络异常而导致操作中断
  9. int i = 10 / 0;
  10. transferRecordDao.transferIn(amount, toUser);
  11. }
  12. }

因为设置了noRollbackFor = ArithmeticException.class,即表示抛出ArithmeticException异常时不会进行回滚

此时运行测试代码,后台会抛出ArithmeticException异常,但不会进行回滚,转账事务中只有出账操作会成功

  1. Exception in thread "main" java.lang.ArithmeticException: / by zero

此时数据库中的数据,就会是下面情况(显然,这并不是我们想要的)

03-JdbcTemplate与声明式事务 - 图26

3.8、声明式事务(XML方式)

  • Step1、配置事务管理器
  1. <!--配置事务管理器-->
  2. <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  3. <property name="dataSource" ref="dataSource"></property>
  4. </bean>
  • Step2、配置事务通知
  1. <!--1、配置事务通知-->
  2. <tx:advice id="txAdvice">
  3. <tx:attributes>
  4. <tx:method name="transferAccounts" propagation="REQUIRED" isolation="REPEATABLE_READ" read-only="false"
  5. timeout="5" rollback-for="java.lang.ArithmeticException"/>
  6. </tx:attributes>
  7. </tx:advice>
  • Step3、配置切入点和切面
  1. <!--2、配置切入点和切面-->
  2. <aop:config>
  3. <aop:pointcut id="p"
  4. expression="execution(* com.vectorx.spring5.s17_transaction_xml.service.TransferRecordService.*(..))"/>
  5. <aop:advisor advice-ref="txAdvice" pointcut-ref="p"></aop:advisor>
  6. </aop:config>

Spring 配置文件整体内容

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xmlns:tx="http://www.springframework.org/schema/tx"
  6. xmlns:aop="http://www.springframework.org/schema/aop"
  7. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  8. http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
  9. http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
  10. http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
  11. <!--配置dataSource-->
  12. <context:property-placeholder location="classpath:jdbc.properties"/>
  13. <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
  14. <property name="driverClassName" value="${mysql.driverClassName}"/>
  15. <property name="url" value="${mysql.url}"/>
  16. <property name="username" value="${mysql.username}"/>
  17. <property name="password" value="${mysql.password}"/>
  18. </bean>
  19. <!--配置JdbcTemplate-->
  20. <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
  21. <!--属性注入dataSource-->
  22. <property name="dataSource" ref="dataSource"></property>
  23. </bean>
  24. <!--配置事务管理器-->
  25. <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  26. <property name="dataSource" ref="dataSource"></property>
  27. </bean>
  28. <!--配置Dao创建和属性注入-->
  29. <bean id="transferRecordDao" class="com.vectorx.spring5.s17_transaction_xml.dao.TransferRecordDaoImpl">
  30. <property name="jdbcTemplate" ref="jdbcTemplate"></property>
  31. </bean>
  32. <!--配置Service创建和属性注入-->
  33. <bean id="transferRecordService" class="com.vectorx.spring5.s17_transaction_xml.service.TransferRecordService">
  34. <property name="transferRecordDao" ref="transferRecordDao"></property>
  35. </bean>
  36. <!--1、配置事务通知-->
  37. <tx:advice id="txAdvice">
  38. <tx:attributes>
  39. <tx:method name="transferAccounts" propagation="REQUIRED" isolation="REPEATABLE_READ" read-only="false"
  40. timeout="5" rollback-for="java.lang.ArithmeticException"/>
  41. </tx:attributes>
  42. </tx:advice>
  43. <!--2、配置切入点和切面-->
  44. <aop:config>
  45. <aop:pointcut id="p"
  46. expression="execution(* com.vectorx.spring5.s17_transaction_xml.service.TransferRecordService.*(..))"/>
  47. <aop:advisor advice-ref="txAdvice" pointcut-ref="p"></aop:advisor>
  48. </aop:config>
  49. </beans>

ServiceDao类去除注解,并对代码稍作修改

  1. public class TransferRecordDaoImpl implements TransferRecordDao {
  2. private JdbcTemplate jdbcTemplate;
  3. public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
  4. this.jdbcTemplate = jdbcTemplate;
  5. }
  6. //...
  7. }
  8. public class TransferRecordService {
  9. private TransferRecordDao transferRecordDao;
  10. public void setTransferRecordDao(TransferRecordDao transferRecordDao) {
  11. this.transferRecordDao = transferRecordDao;
  12. }
  13. //...
  14. }

运行测试代码

后台结果

  1. Exception in thread "main" java.lang.ArithmeticException: / by zero

数据库结果

03-JdbcTemplate与声明式事务 - 图27

3.9、完全注解开发

  1. // 表示此类为配置类
  2. @Configuration
  3. // 自动扫描包
  4. @ComponentScan(basePackages = "com.vectorx.spring5.s18_transaction_annotation")
  5. // 开启事务
  6. @EnableTransactionManagement
  7. // 读取外部配置文件
  8. @PropertySource(value = {"classpath:jdbc.properties"})
  9. public class TxConfig {
  10. @Value(value = "${mysql.driverClassName}")
  11. private String driverClassName;
  12. @Value(value = "${mysql.url}")
  13. private String url;
  14. @Value(value = "${mysql.username}")
  15. private String username;
  16. @Value(value = "${mysql.password}")
  17. private String password;
  18. //配置dataSource
  19. @Bean
  20. public DruidDataSource getDruidDataSource() {
  21. DruidDataSource dataSource = new DruidDataSource();
  22. dataSource.setDriverClassName(driverClassName);
  23. dataSource.setUrl(url);
  24. dataSource.setUsername(username);
  25. dataSource.setPassword(password);
  26. return dataSource;
  27. }
  28. //配置JdbcTemplate
  29. @Bean
  30. public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
  31. //IOC容器会根据类型找到对应DataSource
  32. JdbcTemplate jdbcTemplate = new JdbcTemplate();
  33. jdbcTemplate.setDataSource(dataSource);
  34. return jdbcTemplate;
  35. }
  36. //配置事务管理器
  37. @Bean
  38. public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {
  39. DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
  40. transactionManager.setDataSource(dataSource);
  41. return transactionManager;
  42. }
  43. }

这里我们对各个注解进行一一说明

  • @Configuration:表示此类为一个配置类,其作用等同于创建一个bean.xml,即
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
  5. </beans>
  • @ComponentScan:自动扫描包,basePackages属性配置为需要扫描的包路径,等价于<context:component-scan>标签,即
<!--开启注解扫描-->
<context:component-scan base-package="com.vectorx.spring5.s18_transaction_annotation"/>
  • @EnableTransactionManagement:开启事务管理,等价于<tx:annotation-driven>标签,即
<!--开启事务注解-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
  • @PropertySource:引入外部文件,value配置外部文件路径,等价于<context:property-placeholder>标签
<context:property-placeholder location="classpath:jdbc.properties"/>
  • @Value:对普通类型的属性进行注入,同时其属性值可以使用${}表达式对外部文件配置信息进行获取
  • @Bean:配置对象创建,等价于<bean>标签。可以在被修饰的方法参数列表中传入受IOC容器管理的类型,IOC容器会自动根据类型找到对应对象并注入到此方法中。因此无论是配置JdbcTemplate还是配置事务管理器,都可以使用这种方式对外部Bean进行引用
//配置JdbcTemplate
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource) {...}
//配置事务管理器
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {...}

测试代码

需要注意的是,之前创建的对象是ClassPathXmlApplicationContext,而现在是完全注解开发,所以需要创建的对象是AnnotationConfigApplicationContext,构造参数中传入配置类的class类型即可,其他代码与之前一致

ApplicationContext context = new AnnotationConfigApplicationContext(TxConfig.class);
TransferRecordService transferRecordService = context.getBean("transferRecordService", TransferRecordService.class);
transferRecordService.transferAccounts(100, "Lucy", "Mary");

测试结果

03-JdbcTemplate与声明式事务 - 图28

小结

重点掌握

  • Spring事务管理两种方式:编程式事务管理(不推荐)、声明式事务管理(推荐)
  • Spring事务管理API:PlatformTransactionManagerDataSourceTrasactionManagerHibernateTransactionManager
  • 声明式事务两种实现方式:注解方式和XML方式
  • 事务相关参数有:传播行为、隔离级别、超时时间、是否只读、(不)回滚

    • 传播行为:有7种传播属性,REQUIREDREQUIRED_NEWSUPPORTSNOT_SOPPORTEDMANDATORYNEVERNESTED
    • 隔离级别:有3种典型“读”的问题,脏读、不可重复读、虚(幻)读,可设置4种隔离级别,READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE
    • 其他参数:timeoutreadOnlyrollbackFornoRollbackFor
  • 声明式事务(注解方式):@Transactional
  • 声明式事务(XML方式):配置事务管理器;配置事务通知<tx:advice>;配置切入点和切面
  • 完全注解开发:@EnableTransactionManagement@BeanAnnotationConfigApplicationContext

总结

  1. JdbcTemplateCRUD操作
  2. 事务ACID特性、Spring事务管理
  3. 声明式事务的注解方式和XML方式
  4. 事务相关属性:传播行为、隔离级别、其他参数

下面思维导图经供参考

03-JdbcTemplate与声明式事务 - 图29