具體什么是事務,大家肯定很熟悉,主要目的就是:在並發訪問數據庫的同一資源時,確保 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 包文件。
搭建好的項目工程整體目錄比較簡單,具體如下圖所示:
項目工程結構簡單介紹:
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 進行正常轉賬操作,結果如下圖所示:
然后再執行 transferError 方法,轉賬過程中出現異常錯誤,轉賬失敗,數據回滾到原始狀態,結果如下圖所示:
注意:異常在 juint 的測試方法中,使用 try catch 包裹處理,不要在業務層的方法中進行 try catch 包裹處理。
到此為止,快速從代碼層面介紹完了 Spring 的事務處理,整體來說還是非常簡單的。有了 Spring 事務的支持,我們可以在數據訪問層 Dao 中編寫許多簡單的操作數據庫的方法。當業務層中某個方法有復雜業務邏輯,需要操作數據庫時,我們可以在該業務方法中,采用 Spring 事務調用 Dao 中的相關方法處理,從而使復雜的業務邏輯簡單化。
本博客 Demo 的下載地址為:https://files.cnblogs.com/files/blogs/699532/Spring_Transaction.zip