Spring6(四):JUnit、事务

5. 单元测试:JUnit

在之前的测试方法中,几乎都能看到以下的两行代码:

ApplicationContext context = new ClassPathXmlApplicationContext("xxx.xml");
Xxxx xxx = context.getBean(Xxxx.class);

这两行代码的作用是创建Spring容器,最终获取到对象,但是每次测试都需要重复编写。针对上述问题,我们需要的是程序能自动帮我们创建容器。我们都知道JUnit无法知晓我们是否使用了 Spring 框架,更不用说帮我们创建 Spring 容器了。Spring提供了一个运行器,可以读取配置文件(或注解)来创建容器。我们只需要告诉它配置文件位置就可以了。这样一来,我们通过Spring整合JUnit可以使程序创建spring容器了

5.1 整合JUnit5

项目结构

在这里插入图片描述

pom.xml引入依赖

<dependencies>
    <!--spring context依赖-->
    <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>6.0.2</version>
    </dependency>

    <!--spring对junit的支持相关依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>6.0.2</version>
    </dependency>

    <!--junit5测试-->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.9.0</version>
    </dependency>

    <!--log4j2的依赖-->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>2.19.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j2-impl</artifactId>
        <version>2.19.0</version>
    </dependency>
</dependencies>

bean.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 http://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="com.atguigu.spring6.bean"/>
</beans>

User.java

package com.atguigu.spring6.bean;

import org.springframework.stereotype.Component;

@Component
public class User {

    public User() {
        System.out.println("run user");
    }
}

SpingTestJuint5.java

import com.atguigu.spring6.bean.User;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

//两种方式均可
//方式一
//@ExtendWith(SpringExtension.class)
//@ContextConfiguration("classpath:beans.xml")
//方式二
@SpringJUnitConfig(locations = "classpath:beans.xml")
public class SpringJUnit5Test {

    @Autowired
    private User user;

    @Test
    public void testUser(){
        System.out.println(user);
    }
}

5.2 整合JUnit4

代码延用上面的

添加依赖

<!-- junit测试 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

测试

import com.atguigu.spring6.bean.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:beans.xml")
public class SpringJUnit4Test {

    @Autowired
    private User user;

    @Test
    public void testUser(){
        System.out.println(user);
    }
}

6. 事务

6.1 JdbcTemplate

Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作

6.1.1 准备工作

①搭建子模块

搭建子模块:spring-jdbc-tx

在这里插入图片描述

②加入依赖

<dependencies>
    <!--spring jdbc  Spring 持久化层支持jar包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>6.0.2</version>
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.30</version>
    </dependency>
    <!-- 数据源 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.15</version>
    </dependency>
</dependencies>

③创建jdbc.properties

jdbc.user=root
jdbc.password=root
jdbc.url=jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false
jdbc.driver=com.mysql.cj.jdbc.Driver

④配置Spring的配置文件

beans.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
       http://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.user}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

    <!-- 配置 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 装配数据源 -->
        <property name="dataSource" ref="druidDataSource"/>
    </bean>

</beans>

⑤准备数据库与测试表

CREATE DATABASE `spring`;

use `spring`;

CREATE TABLE `t_emp` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `sex` varchar(2) DEFAULT NULL COMMENT '性别',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Emp.java

public class Emp {

    private Integer id;
    private String name;
    private Integer age;
    private String sex;

    //生成get和set方法 toString方法    不需要生成构造器
}

6.1.2 实现CURD

①装配 JdbcTemplate

创建测试类,整合JUnit,注入JdbcTemplate

package com.atguigu.spring6;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig(locations = "classpath:beans.xml")
public class JDBCTemplateTest {

    @Autowired
    private JdbcTemplate jdbcTemplate;
    
}
②测试增删改功能
@Test
//测试增删改功能
public void testUpdate(){
    //添加功能
	String sql = "insert into t_emp values(null,?,?,?)";
	int result = jdbcTemplate.update(sql, "张三", 23, "男");
    
    //修改功能
	//String sql = "update t_emp set name=? where id=?";
    //int result = jdbcTemplate.update(sql, "张三atguigu", 1);

    //删除功能
	//String sql = "delete from t_emp where id=?";
	//int result = jdbcTemplate.update(sql, 1);
}
③查询数据返回对象
//查询:返回对象
@Test
public void testSelectObject() {
    //写法一
//        String sql = "select * from t_emp where id=?";
//        Emp empResult = jdbcTemplate.queryForObject(sql,
//                (rs, rowNum) -> {
//                    Emp emp = new Emp();
//                    emp.setId(rs.getInt("id"));
//                    emp.setName(rs.getString("name"));
//                    emp.setAge(rs.getInt("age"));
//                    emp.setSex(rs.getString("sex"));
//                    return emp;
//                }, 1);
//        System.out.println(empResult);

    //写法二
    String sql = "select * from t_emp where id=?";
    Emp emp = jdbcTemplate.queryForObject(sql,
                  new BeanPropertyRowMapper<>(Emp.class),1);
    System.out.println(emp);
}
④查询数据返回list集合
@Test
//查询多条数据为一个list集合
public void testSelectList(){
    String sql = "select * from t_emp";
    List<Emp> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Emp.class));
    System.out.println(list);
}
⑤查询返回单个的值
@Test
//查询单行单列的值
public void selectCount(){
    String sql = "select count(id) from t_emp";
    Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
    System.out.println(count);
}

6.2 事务

数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。

6.2.1 编程式事务

事务功能的相关操作全部通过自己编写代码来实现:

Connection conn = ...;
    
try {
    
    // 开启事务:关闭事务的自动提交
    conn.setAutoCommit(false);
    
    // 核心操作
    
    // 提交事务
    conn.commit();
    
}catch(Exception e){
    
    // 回滚事务
    conn.rollBack();
    
}finally{
    
    // 释放数据库连接
    conn.close();
    
}

编程式的实现方式存在缺陷:

  • 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
  • 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。

6.2.2 声明式事务

既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。

封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。

  • 好处1:提高开发效率
  • 好处2:消除了冗余的代码
  • 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化

所以,我们可以总结下面两个概念:

  • 编程式自己写代码实现功能
  • 声明式:通过配置框架实现功能

6.3 基于注解的声明式事务

6.3.1 准备工作

①添加配置

在beans.xml添加配置

<!--扫描组件-->
<context:component-scan base-package="com.atguigu.spring6"></context:component-scan>

②创建表

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:

package com.atguigu.spring6.controller;

@Controller
public class BookController {

    @Autowired
    private BookService bookService;

    public void buyBook(Integer bookId, Integer userId){
        bookService.buyBook(bookId, userId);
    }
}

创建接口BookService:

package com.atguigu.spring6.service;
public interface BookService {
    void buyBook(Integer bookId, Integer userId);
}

创建实现类BookServiceImpl:

package com.atguigu.spring6.service.impl;
@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:

package com.atguigu.spring6.dao;
public interface BookDao {
    Integer getPriceByBookId(Integer bookId);

    void updateStock(Integer bookId);

    void updateBalance(Integer userId, Integer price);
}

创建实现类BookDaoImpl:

package com.atguigu.spring6.dao.impl;
@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);
    }
}

6.3.2 测试无事务情况

①创建测试类

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

@SpringJUnitConfig(locations = "classpath:beans.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

③观察结果

因为没有添加事务,图书的库存更新了,但是用户的余额没有更新

显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败

6.3.3 加入事务

①添加事务配置

在spring配置文件中引入tx命名空间

<?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
       http://www.springframework.org/schema/context/spring-context.xsd
                           
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">

在Spring的配置文件中添加配置:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="druidDataSource"></property>
</bean>

<!--
    开启事务的注解驱动
    通过注解@Transactional所标识的方法或标识的类中所有的方法,都会被事务管理器管理事务
-->
<!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
<tx:annotation-driven transaction-manager="transactionManager" />
②添加事务注解

因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理

在BookServiceImpl的buybook()添加注解@Transactional

③观察结果

由于使用了Spring的声明式事务,更新库存和更新余额都没有执行

6.3.4 @Transactional注解标识的位置

@Transactional标识在方法上,则只会影响该方法

@Transactional标识的类上,则会影响类中所有的方法

6.3.5 事务属性

(1)只读

对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。

@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

(2)超时

事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。

概括来说就是一句话:超时回滚,释放资源。

//超时时间单位秒
@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);
}

③观察结果

执行过程中抛出异常:

org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2022

(3)回滚策略

声明式事务默认只针对运行时异常回滚,编译时异常不回滚。

可以通过@Transactional中相关属性设置回滚策略

  • rollbackFor属性:需要设置一个Class类型的对象

  • rollbackForClassName属性:需要设置一个字符串类型的全类名

  • noRollbackFor属性:需要设置一个Class类型的对象

  • rollbackFor属性:需要设置一个字符串类型的全类名

@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)隔离级别
隔离级别脏读不可重复读幻读
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE
隔离级别OracleMySQL
READ UNCOMMITTED×
READ COMMITTED√(默认)
REPEATABLE READ×√(默认)
SERIALIZABLE

②使用方式

@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化
(5)传播行为

在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。

一共有七种传播行为:

  • REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行**【有就加入,没有就不管了】**
  • MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常**【有就加入,没有就抛异常】**
  • REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起**【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】**
  • NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务**【不支持事务,存在就挂起】**
  • NEVER:以非事务方式运行,如果有事务存在,抛出异常**【不支持事务,存在就抛异常】**
  • NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】

②测试

创建接口CheckoutService:

package com.atguigu.spring6.service;

public interface CheckoutService {
    void checkout(Integer[] bookIds, Integer userId);
}

创建实现类CheckoutServiceImpl:

package com.atguigu.spring6.service.impl;

@Service
public class CheckoutServiceImpl implements CheckoutService {

    @Autowired
    private BookService bookService;

    @Override
    @Transactional
    //一次购买多本图书
    public void checkout(Integer[] bookIds, Integer userId) {
        for (Integer bookId : bookIds) {
            bookService.buyBook(bookId, userId);
        }
    }
}

在BookController中添加方法:

@Autowired
private CheckoutService checkoutService;

public void checkout(Integer[] bookIds, Integer userId){
    checkoutService.checkout(bookIds, userId);
}

在数据库中将用户的余额修改为100元

③观察结果

可以通过@Transactional中的propagation属性设置事务传播行为

修改BookServiceImpl中buyBook()上,注解@Transactional的propagation属性

@Transactional(propagation = Propagation.REQUIRED),默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。经过观察,购买图书的方法buyBook()在checkout()中被调用,checkout()上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了

@Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本。

6.3.6 全注解配置事务

①添加配置类

config/SpringConfig.java

package com.atguigu.spring6.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;

@Configuration
@ComponentScan("com.atguigu.spring6")
@EnableTransactionManagement
public class SpringConfig {

    @Bean
    public DataSource getDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&useSSL=false");
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        return dataSource;
    }

    @Bean(name = "jdbcTemplate")
    public JdbcTemplate getJdbcTemplate(DataSource dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

②测试

import com.atguigu.spring6.config.SpringConfig;
import com.atguigu.spring6.controller.BookController;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

public class TxByAllAnnotationTest {

    @Test
    public void testTxAllAnnotation(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookController accountService = applicationContext.getBean("bookController", BookController.class);
        accountService.buyBook(1, 1);
    }
}

6.4 基于XML的声明式事务

参考基于注解的声明式事务

7.4.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属性:指定方法名,可以使用星号代表多个字符 -->
        <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>6.0.2</version>
</dependency>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Spring框架提供了非常便捷的整合MyBatis和JUnit并进行事务管理的方式。下面是一个简单的示例: 首先,我们需要在Spring配置文件中声明MyBatis的SqlSessionFactory和MapperScannerConfigurer: ``` <!-- 数据源配置 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- MyBatis SqlSessionFactory 配置 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath*:com/example/mapper/*.xml"/> </bean> <!-- MapperScannerConfigurer 配置 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.mapper"/> </bean> ``` 其中,dataSource是数据源配置,sqlSessionFactory是MyBatis的SqlSessionFactory配置,mapperLocations指定了MyBatis的Mapper映射文件所在的路径,MapperScannerConfigurer则是MyBatis的Mapper接口扫描配置。 接下来,我们需要在Spring配置文件中配置事务管理器和事务通知: ``` <!-- 事务管理器配置 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!-- 事务通知配置 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*"/> </tx:attributes> </tx:advice> <aop:config> <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.example.service.*.*(..))"/> </aop:config> ``` 其中,transactionManager是事务管理器的配置,txAdvice是事务通知的配置,用于定义事务的传播行为和隔离级别,aop:advisor则是将事务通知织入Service层的切点中。 最后,我们需要在JUnit测试类中注入Mapper接口和Service实例,并在测试方法中进行事务操作: ``` @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath:spring-config.xml"}) @Transactional public class UserServiceTest { @Autowired private UserMapper userMapper; @Autowired private UserService userService; @Test public void testAddUser() throws Exception { User user = new User(); user.setName("test"); user.setAge(18); userService.addUser(user); User result = userMapper.getUserById(user.getId()); assertNotNull(result); assertEquals(user.getName(), result.getName()); assertEquals(user.getAge(), result.getAge()); } } ``` 其中,@RunWith(SpringJUnit4ClassRunner.class)和@ContextConfiguration(locations = {"classpath:spring-config.xml"})用于加载Spring配置文件,@Transactional用于声明事务,@Autowired用于注入Mapper接口和Service实例,testAddUser方法中则是进行事务操作并进行断言。 通过以上配置和示例,我们就可以完整地实现Spring整合MyBatis和JUnit并进行事务管理了。 ### 回答2: Spring整合MyBatis和JUnit事务管理是一种常见的开发方式,可以使开发人员更方便地进行单元测试和事务管理。 首先,Spring整合MyBatis可以通过配置来实现。在Spring的配置文件中,我们可以定义MyBatis的数据源和SqlSessionFactory,并将其注入到Spring容器中。同时,我们还可以通过配置MapperScannerConfigurer来扫描MyBatis的mapper接口,并自动将其注册到Spring容器中。这样,我们就可以在Spring中使用MyBatis的mapper接口来进行持久化操作。 接下来,使用JUnit进行单元测试。在Spring中,我们可以使用Spring Test模块提供的JUnit运行器来执行我们的单元测试。通过配置@ContextConfiguration注解,我们可以指定需要加载的Spring配置文件,并使用@Autowired注解来自动注入我们需要测试的对象。此外,我们还可以使用@TransactionConfiguration注解来配置事务管理器的一些属性,以保证在测试期间的事务控制。 最后,事务管理是一个非常重要的功能。在Spring中,我们可以通过配置@EnableTransactionManagement注解来启用事务管理功能。通过在方法上添加@Transactional注解,我们可以指定哪些方法需要进行事务管理。在事务控制的过程中,Spring会根据配置来进行事务的提交或回滚操作,以保证数据的一致性和完整性。 总而言之,Spring整合MyBatis和JUnit事务管理是一种灵活而方便的开发方式,可以帮助开发人员更好地进行单元测试和事务管理。通过合理配置,我们可以充分发挥Spring框架的功能,提高开发效率和代码质量。 ### 回答3: Spring整合MyBatis和JUnit事务管理是常见的企业级Java应用开发方案之一。下面我将用300字回答这个问题。 首先,Spring整合MyBatis和JUnit可以方便地进行持久化数据层和业务逻辑层的单元测试。通过使用Spring的测试框架和JUnit,开发人员可以轻松地编写和运行单元测试,以有效地验证代码的正确性和稳定性。通过整合MyBatis,可以使用MyBatis的Mapper接口来访问数据库,并结合Spring的依赖注入功能,将MyBatis的Mapper接口注入到测试类中,方便进行数据库操作的模拟或真实测试。 其次,Spring整合MyBatis还可以简化事务管理。Spring提供了一个统一的事务管理器,可以方便地管理数据库操作的事务,确保数据的完整性和一致性。通过在Spring配置文件中配置事务管理器和事务属性,可以将事务的控制从业务逻辑中解耦出来,使得代码更加清晰和可维护。在JUnit测试中,也可以使用Spring事务管理功能,方便地回滚数据库操作,保持测试环境的干净和一致性。 最后,Spring整合MyBatis和JUnit还可以提高代码的可测试性和可扩展性。通过使用依赖注入和面向接口编程的思想,可以实现代码的解耦和模块化,使得代码更容易被维护和扩展。同时,使用JUnit进行单元测试可以及早地发现和修复代码中的问题,提高代码的质量和稳定性。 综上所述,Spring整合MyBatis和JUnit事务管理是一种强大且灵活的应用开发方案,可以帮助开发人员更加高效和方便地进行单元测试和事务管理,提高代码的质量和可维护性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zdb呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值