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




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM