优质全套Spring全套教程二https://developer.aliyun.com/article/1491439
4.1.3、测试
spring-test:这个是spring整合junit的一个依赖(可以让测试类在spring的测试环境中执行,这样就不用每一次都获取IOC容器了,可以直接依赖注入获取IOC容器中的bean并使用)
需要配置运行环境@RunWith
以后用的多的还是MyBatis,这里了解下
①在测试类装配 JdbcTemplate
- 自动装配就是让应用程序上下文为你找出依赖项的过程。说的通俗一点,就是Spring会在上下文中自动查找,并自动给bean装配与其关联的属性!
//指定当前测试类在Spring的测试环境中执行,此时就可以通过注入的方式直接获取IOC容器中bean @RunWith(SpringJUnit4ClassRunner.class) //设置Spring测试环境的配置文件 @ContextConfiguration("classpath:spring-jdbc.xml") public class JDBCTemplateTest { //通过自动装配的方式为当前属性赋值 @Autowired private JdbcTemplate jdbcTemplate; }
②测试增删改功能
jdbcTemplateTest.java
//指定当前测试类在Spring的测试环境中执行,此时就可以通过注入的方式直接获取IOC容器中bean @RunWith(SpringJUnit4ClassRunner.class) //设置Spring测试环境的配置文件 @ContextConfiguration("classpath:spring-jdbc.xml") public class JdbcTemplateTest { @Autowired private JdbcTemplate jdbcTemplate; @Test public void testInsert(){ String sql = "insert into t_user values(null,?,?,?,?,?)"; jdbcTemplate.update(sql, "root", "123", 23, "女", "123@qq.com"); } @Test public void testGetUserById(){ String sql = "select * from t_user where id = ?"; User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 1);//一个占位符写1个数字 System.out.println(user); } @Test public void testGetAllUser(){ String sql = "select * from t_user"; List<User> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class)); list.forEach(System.out::println); } @Test public void testGetCount(){ String sql = "select count(*) from t_user"; //queryForObject(String sql,Class<T> requiredType)requiredType是需要转换的类型 Integer count = jdbcTemplate.queryForObject(sql, Integer.class); System.out.println(count); } }
③查询一条数据为实体类对象
@Test //查询一条数据为一个实体类对象 public void testSelectEmpById(){ String sql = "select * from t_emp where id = ?"; Emp emp = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Emp.class), 1); System.out.println(emp); }
④查询多条数据为一个list集合
new BeanPropertyRowMapper<>(Emp.class)把查询出来的字段和对应实体类中的属性进行映射
默认映射关系-字段名和属性名一致
@Test //查询多条数据为一个list集合 public void testSelectList(){ String sql = "select * from t_emp"; List<Emp> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Emp.class)); list.forEach(emp -> System.out.println(emp)); }
⑤查询单行单列的值
@Test public void selectCount(){ String sql = "select count(id) from t_emp"; Integer count = jdbcTemplate.queryForObject(sql, Integer.class); System.out.println(count); }
spring-jdbc.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: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 https://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="classpath:jdbc.properties" /> <!-- 配置数据源 --> <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="${jdbc.url}"/> <property name="driverClassName" value="${jdbc.driver}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="druidDataSource"></property> </bean> </beans>
pojo下的User.java
public class User { private Integer id; private String username; private String password; private Integer age; private String gender; private String email; public User() { } public User(Integer id, String username, String password, Integer age, String gender, String email) { this.id = id; this.username = username; this.password = password; this.age = age; this.gender = gender; this.email = email; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } @Override public String toString() { return "User{" + "id=" + id + ", username='" + username + '\'' + ", password='" + password + '\'' + ", age=" + age + ", gender='" + gender + '\'' + ", email='" + email + '\'' + '}'; } }
4.2、声明式事务概念
4.2.1、编程式事务
事务功能的相关操作全部通过自己编写代码来实现:
Connection conn = ...; try { // 开启事务:关闭事务的自动提交 conn.setAutoCommit(false); // 核心操作 // 提交事务 conn.commit(); }catch(Exception e){ // 回滚事务 conn.rollBack(); }finally{ // 释放数据库连接 conn.close(); }
编程式的实现方式存在缺陷:
- 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
4.2.2、声明式事务
spring中有个声明式事务的功能,我们不需要自己写切面和切面的通知
我们只需要把事务管理中切面的通知事务的方法上就可以了
只要和事务相关的都可以通过声明式事务来管理
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
好处1:提高开发效率
好处2:消除了冗余的代码
好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性
能等各个方面的优化
所以,我们可以总结下面两个概念:
编程式:自己写代码实现功能
声明式:通过
4.3、基于注解的声明式事务
4.3.1、准备工作
①加入依赖
<dependencies> <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.1</version> </dependency> <!-- Spring 持久化层支持jar包 --> <!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个jar包 --> <!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>5.3.1</version> </dependency> <!-- Spring 测试相关 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.1</version> </dependency> <!-- junit测试 --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.16</version> </dependency> <!-- 数据源 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.31</version> </dependency> </dependencies>
②创建jdbc.properties
jdbc.user=root jdbc.password=atguigu jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC jdbc.driver=com.mysql.cj.jdbc.Driver
③配置Spring的配置文件
事务需要在JdbcTmplate操作执行SQL语句的过程中来测试,所以还是需要配置JdbcTemplate的
<!--扫描组件--> <context:component-scan base-package="com.atguigu.spring.tx.annotation"> </context:component-scan> <!-- 导入外部属性文件 --> <context:property-placeholder location="classpath:jdbc.properties" /> <!-- 配置数据源 --> <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="${jdbc.url}"/> <property name="driverClassName" value="${jdbc.driver}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- 配置 JdbcTemplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- 装配数据源 --> <property name="dataSource" ref="druidDataSource"/> </bean>
④创建表
unsigned无符号就是无负号,有符号整数范围是-2^31 - 2^31 - 1,无符号整数范围0 - 2^32 - 1(
- 模拟用户买书,查询价格,更新库存,更新余额
CREATE TABLE `t_book` ( `book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `book_name` varchar(20) DEFAULT NULL COMMENT '图书名称', `price` int(11) DEFAULT NULL COMMENT '价格', `stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)', PRIMARY KEY (`book_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍穹',80,100),(2,'斗罗大陆',50,100); CREATE TABLE `t_user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `username` varchar(20) DEFAULT NULL COMMENT '用户名', `balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)', PRIMARY KEY (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);
⑤创建组件
创建BookController:
@Controller public class BookController { @Autowired private BookService bookService; public void buyBook(Integer bookId, Integer userId){ bookService.buyBook(bookId, userId); } }
创建接口BookService:
public interface BookService { void buyBook(Integer bookId, Integer userId); }
创建实现类BookServiceImpl:
@Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao;//这里就要访问数据库了,所以创建持久层对象 @Override public void buyBook(Integer bookId, Integer userId) { //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); } }
创建接口BookDao:
public interface BookDao { Integer getPriceByBookId(Integer bookId); void updateStock(Integer bookId); void updateBalance(Integer userId, Integer price); }
创建实现类BookDaoImpl:
@Repository public class BookDaoImpl implements BookDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public Integer getPriceByBookId(Integer bookId) { String sql = "select price from t_book where book_id = ?"; return jdbcTemplate.queryForObject(sql, Integer.class, bookId); } @Override public void updateStock(Integer bookId) { String sql = "update t_book set stock = stock - 1 where book_id = ?";//MySQL里面是没有+=,-=这种java语法的 jdbcTemplate.update(sql, bookId);//填充占位符 } @Override public void updateBalance(Integer userId, Integer price) { String sql = "update t_user set balance = balance - ? where user_id =?"; jdbcTemplate.update(sql, price, userId);//对应两个?price,userId } }
4.3.2、测试无事务情况
在MySQL中一个sql独占一个事务且自动提交,一定要关闭自动提交,不然可能导致更新了图书库存,但是余额那里除了问题
- 我们设置了unsigned,数据是不能为负号的,
①创建测试类
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:tx-annotation.xml") public class TxByAnnotationTest { @Autowired private BookController bookController; @Test public void testBuyBook(){ bookController.buyBook(1, 1); } }
②模拟场景
用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额
假设用户id为1的用户,购买id为1的图书
用户余额为50,而图书价格为80
购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段
此时执行sql语句会抛出SQLException
③观察结果
因为没有添加事务,图书的库存更新了,但是用户的余额没有更新
显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败
4.3.3、加入事务
①添加事务配置
tx:annotation-driven是将事务管理器里面的切面通知作用到当前的连接点上的(我理解为桥梁),@Transactional加在了哪一个方法上,哪一个方法就是连接点,如果加在类上,类中所有的方法都是连接点;
切面的通知直接定位到连接点上(也就是定位到需要被管理的方法上-这里的通知指的是事务中的东西-commit等)
事务管理器是TransactionManager,而它是个接口,我们需要找它的实现类,它的实现类就是DataSourceTransactionManager数据源事务管理器
要想处理事务,必须要有数据源,数据源是来管理连接的,事务相关的所有代码都是通过连接对象来设置的,都是Connection点.什么什么,
所以事务管理是需要依赖于数据源的
在Spring的配置文件中添加配置:
<!--这就是一个切面transactionManager,这里面才是通知,它就可以将通知作用到加注解的地方--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 开启事务的注解驱动 通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务(哪里加了这个注解,哪里就是连接点);这个tx:annotation-driven的作用就是把通知作用到连接点上 --> <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就 是这个默认值,则可以省略这个属性 (写的就是事务管理器的id)--> <tx:annotation-driven transaction-manager="transactionManager"/>
注意:导入的名称空间需要 tx 结尾的那个。
②添加事务注解
因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理
在BookServiceImpl的buybook()添加注解@Transactional
③观察结果
由于使用了Spring的声明式事务,更新库存和更新余额都没有执行
4.3.4、@Transactional注解标识的位置
@Transactional标识在方法上,咋只会影响该方法
@Transactional标识的类上,咋会影响类中所有的方法
4.3.5、事务属性:只读
事务中都是查询的时候才能设置事物的只读,有任意一个增删改都不能用只读
①介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
②使用方式
@Transactional(readOnly = true) public void buyBook(Integer bookId, Integer userId) { //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); //System.out.println(1/0); }
③注意
对增删改操作设置只读会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification
are not allowed
4.3.6、事务属性:超时
①介绍
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。
概括来说就是一句话:超时回滚,释放资源。
②使用方式
5>3 ,3秒之后就会回滚,在规定时间内没有执行完就会强制回滚
@Transactional(timeout = 3) public void buyBook(Integer bookId, Integer userId) { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); //System.out.println(1/0); } /** * Date:2022/1/2 * Author:即兴小索奇 * Description: * 声明式事务的配置步骤: * 1、在Spring的配置文件中配置事务管理器 * 2、开启事务的注解驱动 * 在需要被事务管理的方法上,添加@Transactional注解,该方法就会被事务管理 * @Transactional注解标识的位置: * 1、标识在方法上 * 2、标识在类上,则类中所有的方法都会被事务管理 */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:tx-annotation.xml") public class TxByAnnotationTest { @Autowired private BookController bookController; @Test public void testBuyBook(){ //bookController.buyBook(1, 1); bookController.checkout(1, new Integer[]{1,2}); } }
③观察结果
执行过程中抛出异常:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out:
deadline was Fri Jun 04 16:25:39 CST 2022
4.3.7、事务属性:回滚策略
①介绍
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
可以通过@Transactional中相关属性设置回滚策略
rollbackFor属性:需要设置一个Class类型的对象(因为什么而回滚,写异常所对应的class对象)
rollbackForClassName属性:需要设置一个字符串类型的全类名(因为什么而回滚,写全类名)
noRollbackFor属性:需要设置一个Class类型的对象(不因为什么而回滚)
norollbackForClassName属性:需要设置一个字符串类型的全类名
一般只设置no开头的两个,因为因为什么而回滚,我们声明式事务中默认情况下都是针对于运行时异常而进行回滚的,默认就是只要是运行时异常都进行回滚
noRollbackForClassName后面是{}写数组的,如果写一个可以省略{}
②使用方式
@Transactional(noRollbackFor = ArithmeticException.class) //@Transactional(noRollbackForClassName = "java.lang.ArithmeticException") public void buyBook(Integer bookId, Integer userId) { //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); System.out.println(1/0); }
③观察结果
虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当
出现ArithmeticException不发生回滚,因此购买图书的操作正常执行
4.3.8、事务属性:事务隔离级别
①介绍
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事
务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同
的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。
读已提交:READ COMMITTED、
要求Transaction01只能读取Transaction02已提交的修改。
比如02开启添加了新的事务,添加了一个用户信息,01事务是读取不到的,只有02提交了事务才能读取到
可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值(加锁),即Transaction01执行期间禁止其它事务对这个字段进行更新。(如果操作就会一直处于堵塞状态,直到01提交事务)
但是01可以读取这个事务被其它事务修改的数据,可能造成Transaction01已加锁的数据间接被修改,01能读到这些数据,这就造成了幻读
幻读是指在一个事务中,由于其他事务对数据的修改,导致同一个事务中多次读取同一数据时,读取结果不一致的现象。
MySQL的可重复读就已经避免了幻读(这是MySQL中最理想的隔离级别),01事务读取不到02事务修改的数据
弹幕:MySQL的innodb存储引擎已经通过多版本并发机制解决了幻读的问题
串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它
事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
各个隔离级别解决并发问题的能力见下表:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
READ UNCOMMITTED | 有 | 有 | 有 |
READ COMMITTED | 无 | 有 | 有 |
REPEATABLE READ | 无 | 无 | 有 |
SERIALIZABLE | 无 | 无 | 无 |
各种数据库产品对事务隔离级别的支持程度:
隔离级别 | Oracle | MySQL |
READ UNCOMMITTED | × | √ |
READ COMMITTED | √(默认) | √ |
REPEATABLE READ | × | √(默认) |
SERIALIZABLE | √ | √ |
②使用方式
isolation(Isolation isolation()是枚举类型)
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别 @Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交 @Transactional(isolation = Isolation.READ_COMMITTED)//读已提交 @Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读 @Transactional(isolation = Isolation.SERIALIZABLE)//串行化
4.3.9、事务属性:事务传播行为
面试经常问!
①介绍
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
A事务调用B事务的时候,A就会把它的事务传播到B中,B如何使用这个事务,这就叫做事务传播行为
比如两本书,比如用结账的事务,两本书有一本买不了,都买不了,如果用买书本身的事务,能买几本就买几本
②测试
BookController.java @Controller public class BookController { @Autowired private BookService bookService; @Autowired private CheckoutService checkoutService; public void buyBook(Integer userId, Integer bookId){ bookService.buyBook(userId, bookId); } public void checkout(Integer userId, Integer[] bookIds){ checkoutService.checkout(userId, bookIds); } }
创建接口CheckoutService:
public interface CheckoutService { void checkout(Integer[] bookIds, Integer userId ); }
创建实现类CheckoutServiceImpl:
注意这里自动装配的是CheckoutServiceImpl,调用的是这个类里面的checkout方法
@Service public class CheckoutServiceImpl implements CheckoutService { @Autowired private BookService bookService; @Override //@Transactional public void checkout(Integer userId, Integer[] bookIds) { for (Integer bookId : bookIds) { bookService.buyBook(userId, bookId); } } }
在BookController中添加方法:
@Autowired private CheckoutService checkoutService; public void checkout(Integer[] bookIds, Integer userId){ checkoutService.checkout(bookIds, userId); } BookDao public interface BookDao{ void updateStock(Integer bookId); void updateBalance(Integer userId, Integer price); Integer getPriceByBookId(Integer bookId); }
BookDaoImpl
@Repository public class BookDaoImpl implements BookDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public Integer getPriceByBookId(Integer bookId) { String sql = "select price from t_book where book_id = ?"; return jdbcTemplate.queryForObject(sql, Integer.class, bookId); } @Override public void updateStock(Integer bookId) { String sql = "update t_book set stock = stock - 1 where book_id = ?"; jdbcTemplate.update(sql, bookId); } @Override public void updateBalance(Integer userId, Integer price) { String sql = "update t_user set balance = balance - ? where user_id = ?"; jdbcTemplate.update(sql, price, userId); } }
在数据库中将用户的余额修改为100元
tx-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:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:component-scan base-package="com.atguigu.spring"></context:component-scan> <context:property-placeholder location="classpath:jdbc.properties" /> <!-- 配置数据源 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="${jdbc.url}"/> <property name="driverClassName" value="${jdbc.driver}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"></property> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 开启事务的注解驱动 通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务 --> <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就 是这个默认值,则可以省略这个属性 --> <tx:annotation-driven transaction-manager="transactionManager" /> </beans>
测试方法
注意balance不能低于书本的价格,不然报错,设置的是unsigned
/** * Date:2022/7/6 * Author:ybc * Description: * 声明式事务的配置步骤: * 1、在Spring的配置文件中配置事务管理器 * 2、开启事务的注解驱动 * 在需要被事务管理的方法上,添加@Transactional注解,该方法就会被事务管理 * @Transactional注解标识的位置: * 1、标识在方法上 * 2、标识在类上,则类中所有的方法都会被事务管理 */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:tx-annotation.xml") public class TxByAnnotationTest { @Autowired private BookController bookController; @Test public void testBuyBook(){ //bookController.buyBook(1, 1); bookController.checkout(1, new Integer[]{1,2}); } }
③观察结果
买书和结账都设置了transactional,默认使用的是结账的事务(只要有一本买不了整个事务回滚)
可以通过@Transactional中的propagation属性设置事务传播行为
修改BookServiceImpl中buyBook()上,注解@Transactional的propagation属性
@Transactional(propagation = Propagation.REQUIRED)使用的是调用的事务,默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行(这里用的是结账的事务)。经过观察,购买图书的方法buyBook()在checkout()中被调
用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了
@Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本
Propagation设置事物的传播行为的(动植物等的)繁殖,增殖,;(观点、理论等的)传播;(运动、光线、声音等的)传送
4.4、基于XML的声明式事务
4.3.1、场景模拟
参考基于注解的声明式事务
4.3.2、修改Spring配置文件
将Spring配置文件中去掉tx:annotation-driven 标签,并添加配置:
<aop:config> <!-- 配置事务通知和切入点表达式 --> <aop:advisor advice-ref="txAdvice" pointcut="execution(*com.atguigu.spring.tx.xml.service.impl.*.*(..))"></aop:advisor> </aop:config> <!-- tx:advice标签:配置事务通知 --> <!-- id属性:给事务通知标签设置唯一标识,便于引用 --> <!-- transaction-manager属性:关联事务管理器 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- tx:method标签:配置具体的事务方法 --> <!-- name属性:指定方法名,可以使用星号代表多个字符,get*包括所有以get开头的方法名,自己设置,按照自己命名规则,下面任选 --> <!--name也可以直接写一个*,代表所有切入点表达式--> <tx:method name="get*" read-only="true"/> <tx:method name="query*" read-only="true"/> <tx:method name="find*" read-only="true"/> <!-- read-only属性:设置只读属性 --> <!-- rollback-for属性:设置回滚的异常 --> <!-- no-rollback-for属性:设置不回滚的异常 --> <!-- isolation属性:设置事务的隔离级别 --> <!-- timeout属性:设置事务的超时属性 --> <!-- propagation属性:设置事务的传播行为 --> <tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/> <tx:method name="update*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/> <tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/> </tx:attributes> </tx:advice>
注意:基于xml实现的声明式事务,必须引入aspectJ的依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.1</version> </dependency>