优质全套Spring全套教程三

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 优质全套Spring全套教程三

优质全套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>
相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
17天前
|
JSON Java Maven
实现Java Spring Boot FCM推送教程
本指南介绍了如何在Spring Boot项目中集成Firebase云消息服务(FCM),包括创建项目、添加依赖、配置服务账户密钥、编写推送服务类以及发送消息等步骤,帮助开发者快速实现推送通知功能。
46 2
|
2月前
|
XML JavaScript Java
Spring Retry 教程
Spring Retry 是 Spring 提供的用于处理方法重试的库,通过 AOP 提供声明式重试机制,不侵入业务逻辑代码。主要步骤包括:添加依赖、启用重试机制、设置重试策略(如异常类型、重试次数、延迟策略等),并可定义重试失败后的回调方法。适用于因瞬时故障导致的操作失败场景。
Spring Retry 教程
|
1月前
|
JSON Java Maven
实现Java Spring Boot FCM推送教程
详细介绍实现Java Spring Boot FCM推送教程
87 0
|
3月前
|
Java 数据库连接 Spring
一文讲明 Spring 的使用 【全网超详细教程】
这篇文章是一份全面的Spring框架使用教程,涵盖了从基础的项目搭建、IOC和AOP概念的介绍,到Spring的依赖注入、动态代理、事务处理等高级主题,并通过代码示例和配置文件展示了如何在实际项目中应用Spring框架的各种功能。
一文讲明 Spring 的使用 【全网超详细教程】
|
4月前
|
NoSQL Java 数据库连接
《滚雪球学Spring Boot》教程导航帖(更新于2024.07.16)
📚 《滚雪球学Spring Boot》是由CSDN博主bug菌创作的全面Spring Boot教程。作者是全栈开发专家,在多个技术社区如CSDN、掘金、InfoQ、51CTO等担任博客专家,并拥有超过20万的全网粉丝。该教程分为入门篇和进阶篇,每篇包含详细的教学步骤,涵盖Spring Boot的基础和高级主题。
65 1
《滚雪球学Spring Boot》教程导航帖(更新于2024.07.16)
|
3月前
|
SQL Java 数据库连接
Spring Boot联手MyBatis,打造开发利器:从入门到精通,实战教程带你飞越编程高峰!
【8月更文挑战第29天】Spring Boot与MyBatis分别是Java快速开发和持久层框架的优秀代表。本文通过整合Spring Boot与MyBatis,展示了如何在项目中添加相关依赖、配置数据源及MyBatis,并通过实战示例介绍了实体类、Mapper接口及Controller的创建过程。通过本文,你将学会如何利用这两款工具提高开发效率,实现数据的增删查改等复杂操作,为实际项目开发提供有力支持。
155 0
|
3月前
|
Java 关系型数据库 MySQL
|
4月前
|
Java 索引 Spring
教程:Spring Boot中集成Elasticsearch的步骤
教程:Spring Boot中集成Elasticsearch的步骤
|
4月前
|
Java API Spring
教程:Spring Boot中如何集成GraphQL
教程:Spring Boot中如何集成GraphQL
|
4月前
|
缓存 Java Spring
教程:Spring Boot中集成Memcached的详细步骤
教程:Spring Boot中集成Memcached的详细步骤
下一篇
无影云桌面