Spring 采用纯注解实现业务层事务处理

具体什么是事务,大家肯定很熟悉,主要目的就是:在并发访问数据库的同一资源时,确保 ACID(原子性、一致性、隔离性、持久性)。简单理解就是如果一次性对数据库进行多个操作(主要是写操作),事务可以确保本次的多个写操作,要么全部成功,要么全部失败。有关事务的理论知识,请大家自行查找资料学习,本篇博客重点在于代码实践。

虽然数据库本身可以通过 Sql 语句编写事务操作,但是这不在本篇博客的介绍范围中。本篇博客所介绍的 Spring 事务,是在业务层代码中进行事务处理,在实际开发场景中的代码实现,非常简单。在本篇博客的最后会提供 Demo 的源代码。


一、搭建工程

新建一个 maven 项目,导入相关 jar 包,我导入的都是最新版本的 jar 包,内容如下:

有关具体的 jar 包地址,可以在 https://mvnrepository.com 上进行查询。

<dependencies>
    <!--Spring 相关的 jar 包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.17</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.3.17</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.3.17</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>

    <!--Mysql 和数据库连接池相关的 jar 包-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.28</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.8</version>
    </dependency>

    <!--Mybatis 相关的 jar 包-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.9</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.0.7</version>
    </dependency>
</dependencies>

打开右侧的 Maven 窗口,刷新一下,这样 Maven 会自动下载所需的 jar 包文件。

搭建好的项目工程整体目录比较简单,具体如下图所示:

image

项目工程结构简单介绍:

config 包下存放的是 Spring 的配置类

dao 包下存放的是 Mybatis 操作数据库的接口类

domain 包下存放是具体的 Java Bean 实体对象类

service 包下存放的是业务处理实现类

resources 目录下存放的是连接数据库的相关参数的配置文件

test 目录下 BankUserServiceTest 这个是测试方法类,里面编写了测试 Spring 业务层事务处理的方法

说明:本 Demo 采用 Mybatis 操作数据库,简单模拟两个银行账户的转账操作。在转账的业务层方法中,使用 Spring 的事务处理,确保转账过程的数据一致性,即:对两个银行账户的加钱和减钱操作,要么都成功,要么都失败。


二、配置相关细节

在本机的 mysql 中运行以下 sql 脚本,进行数据库环境的准备工作,内容如下:

CREATE DATABASE IF NOT EXISTS `testdb` 
USE `testdb`;

CREATE TABLE IF NOT EXISTS `bankuser` (
  `id` int(11) NOT NULL,
  `name` varchar(50) DEFAULT NULL,
  `money` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `bankuser` (`id`, `name`, `money`) VALUES
	(1, '任肥肥', 3000),
	(2, '侯胖胖', 3000),
	(3, '乔豆豆', 3000);

在 resources 目录下的 jdbc.properties 文件配置数据库连接信息,内容如下:

mysql.driver=com.mysql.cj.jdbc.Driver
mysql.url=jdbc:mysql://localhost:3306/testdb?useSSL=false
mysql.username=root
mysql.password=123456

# 初始化连接的数量
druid.initialSize=3
# 最大连接的数量
druid.maxActive=20
# 获取连接的最大等待时间(毫秒)
druid.maxWait=3000

在 config 包下,编写 Jdbc 连接数据库的配置类,Mybatis 的配置类,以及 Spring 的配置类

package com.jobs.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

//通过 @PropertySource 注解,加载 jdbc.properties 文件的配置信息
@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {

    //通过 @Value 注解,获取 jdbc.properties 文件中相关 key 的配置值
    @Value("${mysql.driver}")
    private String driver;
    @Value("${mysql.url}")
    private String url;
    @Value("${mysql.username}")
    private String userName;
    @Value("${mysql.password}")
    private String password;

    @Value("${druid.initialSize}")
    private Integer initialSize;
    @Value("${druid.maxActive}")
    private Integer maxActive;
    @Value("${druid.maxWait}")
    private Long maxWait;

    //让 Spring 装载 druid 具有数据库连接池的数据源
    @Bean
    public DataSource getDataSource(){
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        ds.setInitialSize(initialSize);
        ds.setMaxActive(maxActive);
        ds.setMaxWait(maxWait);
        return ds;
    }

    //让 Spring 装载 jdbc 的事务管理器
    @Bean
    public PlatformTransactionManager getTransactionManager(@Autowired DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }
}
package com.jobs.config;

import org.apache.ibatis.logging.stdout.StdOutImpl;
import org.apache.ibatis.session.Configuration;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;

public class MybatisConfig {

    //让 Spring 装载 SqlSessionFactoryBean
    @Bean
    public SqlSessionFactoryBean getSqlSessionFactoryBean(@Autowired DataSource dataSource){
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setTypeAliasesPackage("com.jobs.domain");
        ssfb.setDataSource(dataSource);

        /*
            //这里的配置,让 MyBatis 在运行时,在控制台打印所执行的 sql 语句,方便排查问题
            //由于本 demo 所执行的 sql 语句很简单,所以就注释这里的配置了
            Configuration mybatisConfig = new Configuration();
            mybatisConfig.setLogImpl(StdOutImpl.class);
            ssfb.setConfiguration(mybatisConfig);
        */

        return ssfb;
    }

    //让 Spring 装载操作数据库的接口映射扫描器
    @Bean
    public MapperScannerConfigurer getMapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.jobs.dao");
        return msc;
    }
}
package com.jobs.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@ComponentScan("com.jobs")
@Import({JdbcConfig.class,MybatisConfig.class})
//启用 Spring 的事务管理
@EnableTransactionManagement
public class SpringConfig {
}

以上配置类中,需要注意的是:要想启用 Spring 的事务支持,需要让 Spring 装载 Jdbc 的事务管理器,需要在 Spring 的主配置类上面使用 @EnableTransactionManagement 注解,启用 Spring 的事务支持。


三、事务处理细节

首先列出 domain 包下银行账户的实体类细节,内容如下:

package com.jobs.domain;

public class BankUser {

    private Integer id;
    private String name;
    private Integer money;

    public BankUser() {
    }

    public BankUser(Integer id, String name, Integer money) {
        this.id = id;
        this.name = name;
        this.money = money;
    }

    //此处省略了 get 和 set 方法的细节....

    @Override
    public String toString() {
        return "BankUser{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", money=" + money +
                '}';
    }
}

Mybatis 操作数据库的 Mapper 接口,其中转账分为了 2 个方法,一个是加钱,一个是减钱,具体细节如下:

package com.jobs.dao;

import com.jobs.domain.BankUser;
import org.apache.ibatis.annotations.*;

import java.util.List;

public interface BankUserDao {

    //给银行账户加钱
    @Update("update bankuser set money = money + #{money} where name = #{name}")
    void addMoney(@Param("name") String name, @Param("money") Integer money);

    //给银行账户减钱
    @Update("update bankuser set money = money - #{money} where name = #{name}")
    void minusMoney(@Param("name") String name, @Param("money") Integer money);

    //获取所有的银行账户,按照 id 升序排列
    @Results(id = "bankuser_map", value = {
            @Result(column = "id", property = "id"),
            @Result(column = "name", property = "name"),
            @Result(column = "money", property = "money")})
    @Select("select id,name,money from bankuser order by id")
    List<BankUser> getBankUserList();
}

业务层的接口内容细节如下,需要注意的是 Spring 的事务是通过 @Transactional 注解,加持在业务层上的。最好将注解放在接口上,不要放在具体的实现类上,因为这样做的好处是:后续如果接口有新的实现类时,实现类自动具有了跟接口一样的事务。

@Transactional 注解,可以直接放在接口上(接口内所有的方法都需要事务),也可以放在具体的接口方法上。

package com.jobs.service;

import com.jobs.domain.BankUser;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

//可以在接口上增加 @Transactional 注解
//表示实现该接口的类中的所有方法,都需要事务处理
//@Transactional
public interface BankUserService {

    //可以在具体的接口方法上使用 @Transactional 注解,表示该方法需要事务处理
    @Transactional
    void transferMoney(String fromBankUser, String toBankUser, Integer money);

    //这里也实现银行转账功能,但是故意转账失败,验证事务是否回滚
    @Transactional
    void transferError(String fromBankUser, String toBankUser, Integer money);

    //获取所有的银行账户,按照 id 升序排列
    List<BankUser> getBankUserList();
}

接口的实现细节为:

package com.jobs.service.impl;

import com.jobs.dao.BankUserDao;
import com.jobs.domain.BankUser;
import com.jobs.service.BankUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class BankUserServiceImpl implements BankUserService {

    //由于 Dao 对象是 Mybatis 在运行时自动装载到 Spring 容器中,所以 IDEA 检测不到 Dao 对象
    //因此这里使用 @Autowired 按类型注入时,IEDA 会提示找不到该类型的 Bean 对象,不用理会 IDEA 的提示
    //如果你实在是看着 IDEA 的红色波浪线提示,很心烦的话,可以使用 @Resource 注解进行代替
    //@Autowired
    @Resource(type = BankUserDao.class)
    private BankUserDao bankUserDao;

    //银行账户转账
    @Override
    public void transferMoney(String fromBankUser, String toBankUser, Integer money) {
        bankUserDao.minusMoney(fromBankUser, money);
        bankUserDao.addMoney(toBankUser, money);
    }

    //这里也实现银行转账功能,但是故意转账失败,验证事务是否回滚
    @Override
    public void transferError(String fromBankUser, String toBankUser, Integer money) {

        //注意:请在该方法的调用者中添加 try catch 处理,千万不要在这里使用 try catch 处理
        //因为如果使用了 try catch 包裹,这里就不会向调用者抛异常了
        //如果没有抛异常,那么就会认为业务层的事务执行成功。

        bankUserDao.minusMoney(fromBankUser, money);
        int result = 1 / 0;
        bankUserDao.addMoney(toBankUser, money);
    }

    @Override
    public List<BankUser> getBankUserList() {
        List<BankUser> bankUserList = bankUserDao.getBankUserList();
        return bankUserList;
    }
}

接口的实现类需要注意的细节是:不要在有可能出错的地方,使用 try catch 包裹并处理异常,这一点很重要,因为一旦业务方法没有出错,Spring 就认为事务执行成功了。所以建议在业务方法的调用者中,使用 try catch 包裹处理,不要在业务层方法中使用 try catch 包裹处理。


四、测试验证 Spring 事务

在 test 目录下的 BankUserServiceTest 类中,编写转账测试的方法,内容如下:

package com.jobs;

import com.jobs.config.SpringConfig;
import com.jobs.domain.BankUser;
import com.jobs.service.BankUserService;
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;

import java.util.List;

//Spring 集成 JUint 测试
@RunWith(SpringJUnit4ClassRunner.class)
//指定 Spring 配置类,以便在 JUnit 中使用 Spring 容器的 Bean 对象
@ContextConfiguration(classes = SpringConfig.class)
public class BankUserServiceTest {

    @Autowired
    private BankUserService bankUserService;

    //在业务层使用事务,正常转账,能够成功
    @Test
    public void transferMoney() {

        List<BankUser> bankUserListBefore = bankUserService.getBankUserList();
        System.out.println("转账前,银行账户信息:");
        for (BankUser bu : bankUserListBefore) {
            System.out.println(bu);
        }

        System.out.println("=======================");

        System.out.println("【任肥肥】向【乔豆豆】转账【1000】元...");
        bankUserService.transferMoney("任肥肥", "乔豆豆", 1000);

        System.out.println("=======================");

        List<BankUser> bankUserListAfter = bankUserService.getBankUserList();
        System.out.println("转账后,银行账户信息:");
        for (BankUser bu : bankUserListAfter) {
            System.out.println(bu);
        }
    }

    //在业务层使用事务,转账过程中出现问题,事务回滚
    @Test
    public void transferError() {

        List<BankUser> bankUserListBefore = bankUserService.getBankUserList();
        System.out.println("转账前,银行账户信息:");
        for (BankUser bu : bankUserListBefore) {
            System.out.println(bu);
        }

        System.out.println("=======================");

        System.out.println("【侯胖胖】向【乔豆豆】转账【1000】元...");
        try {
            bankUserService.transferError("侯胖胖", "乔豆豆", 1000);
        } catch (Exception ex) {
            System.out.println("转账失败,失败原因:" + ex.getMessage());
        }

        System.out.println("=======================");

        List<BankUser> bankUserListAfter = bankUserService.getBankUserList();
        System.out.println("转账出错后,银行账户信息:");
        for (BankUser bu : bankUserListAfter) {
            System.out.println(bu);
        }
    }
}

先执行 transferMoney 进行正常转账操作,结果如下图所示:

image

然后再执行 transferError 方法,转账过程中出现异常错误,转账失败,数据回滚到原始状态,结果如下图所示:
注意:异常在 juint 的测试方法中,使用 try catch 包裹处理,不要在业务层的方法中进行 try catch 包裹处理。

image



到此为止,快速从代码层面介绍完了 Spring 的事务处理,整体来说还是非常简单的。有了 Spring 事务的支持,我们可以在数据访问层 Dao 中编写许多简单的操作数据库的方法。当业务层中某个方法有复杂业务逻辑,需要操作数据库时,我们可以在该业务方法中,采用 Spring 事务调用 Dao 中的相关方法处理,从而使复杂的业务逻辑简单化。

本博客 Demo 的下载地址为:https://files.cnblogs.com/files/blogs/699532/Spring_Transaction.zip



posted @ 2022-03-27 14:13  乔京飞  阅读(9779)  评论(0编辑  收藏  举报