在之前的两篇文章《基于XML配置的方式实现Spring中的AOP》《基于注解的方式实现Spring中的AOP》中我们分别通过XML配置和注解配置的方式实现了自定义的事务控制,其实Spring框架已经为我们提供了事务管理功能。本文将继续使用之前的案例来介绍Spring中的事务控制。

一、Spring事务控制的API

在介绍如何使用Spring为我们提供的事务控制之前,我们先来了解一下Spring事务控制的相关API

1、PlatformTransactionManager

PlatformTransactionManager接口提供事务操作的方法,包含有3个具体的操作,下面通过一张表格来说明:

方法名 说明
TransactionStatus getTransaction(TransactionDefinition definition) 获取事务状态信息
void commit(TransactionStatus status) 提交事务
void rollback(TransactionStatus status) 回滚事务

在实际开发中都是使用该接口的实现类,下面列出两个常用的实现类:

  • org.springframework.jdbc.datasource.DataSourceTransactionManager:当使用Spring JDBC 或 iBatis 进行持久化数据时使用
  • org.springframework.orm.hibernate5.HibernateTransactionManager:当使用hibernate 进行持久化数据时使用

2、TransactionDefinition

TransactionDefinition是事务的定义信息对象,其中有如下方法:

方法名 说明
String getName() 获取事务对象名称
int getIsolationLevel() 获取事务隔离级别
int getPropagationBehavior() 获取事务传播行为
int getTimeout() 获取事务超时时间
boolean isReadOnly() 获取事务是否只读

下面将对其中的一些细节进行介绍:

(1)事务的隔离级别

事务的隔离级别反映了事务提交并发访问时的处理态度

事务的四种隔离级别分别为:未提交读(Read Uncommitted)、已提交读(Read Committed)、可重复读(Repeatable Read)和可串行化(Serializable),下面将通过一张表格来说明这四种隔离级别的区别:

隔离级别 脏读(Dirty Read) 幻读(Phantom Read)
未提交读 可能 可能
已提交读 不可能 可能
可重复读 不可能 可能
可串行化 不可能 不可能

更多关于事务隔离级别的内容,请参阅:《彻底搞懂 MySQL 事务的隔离级别》《MySQL 事务隔离级别和锁》

TransactionDefinition接口中提供了几种事务隔离级别,下面将通过表格进行说明:

隔离级别 说明
ISOLATION_DEFAULT -1 默认级别,和数据库保持一致
ISOLATION_READ_UNCOMMITTED 1 未提交读
ISOLATION_READ_COMMITTED 2 已提交读(Oracle默认级别)
ISOLATION_REPEATABLE_READ 4 可重复读(MySQL默认级别)
ISOLATION_SERIALIZABLE 8 可串行化
(2)事务的传播行为

事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播

Spring 在TransactionDefinition接口中规定了7种类型的事务传播行为。事务传播行为是Spring框架独有的事务增强特性,他不属于的事务实际提供方数据库行为。下面将通过表格来对事务传播行为进行说明:

事务传播行为类型 说明
PROPAGATION_REQUIRED 0 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中(默认值)
PROPAGATION_SUPPORTS 1 支持当前事务,如果当前没有事务,就以非事务方式执行
PROPAGATION_MANDATORY 2 使用当前的事务,如果当前没有事务,就抛出异常
PROPAGATION_REQUIRES_NEW 3 新建事务,如果当前存在事务,把当前事务挂起
PROPAGATION_NOT_SUPPORTED 4 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
PROPAGATION_NEVER 5 以非事务方式执行,如果当前存在事务,则抛出异常
PROPAGATION_NESTED 6 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作
(3)超时时间

默认值为-1,即没有超时限制。如果有,则以秒为单位进行设置。

(4)是否为只读事务

建议在查询时设置为只读。

3、TransactionStatus

TransactionStatus接口描述了某个时间点上事务对象的状态信息,包含6个具体的操作,下面将通过表格来介绍这些操作:

方法名 说明
void flush() 刷新事务
boolean hasSavePoint() 是否存在存储点
boolean isCompleted() 事务是否已经完成
boolean isNewTransaction() 事务是否为新的事务
boolean isRollbackOnly() 事务是否回滚
void setRollbackOnly() 设置事务回滚

二、基于XML声明式事务控制

下面我们将通过一个案例来介绍如何基于XML的方式来实现事务控制。在之前案例的不同之处在于,这次我们采用Spring JDBC来进行数据库相关操作。下面给出相关代码:

(1)数据访问层的实现类AccountDaoImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package cn.frankfang.dao.impl;

import cn.frankfang.dao.IAccountDao;
import cn.frankfang.pojo.Account;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.support.JdbcDaoSupport;

import java.util.List;

/**
* 数据访问层实现类
*/
public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao {

public AccountDaoImpl(JdbcTemplate jdbcTemplate) {
super.setJdbcTemplate(jdbcTemplate);
}

@Override
public int saveAccount(Account account) {
try{
return super.getJdbcTemplate().update("insert into account(id,name,money)values(?,?,?)",
account.getId(),
account.getName(),
account.getMoney());
}catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
public Account selectById(Integer id) {
try {
List<Account> accounts = super.getJdbcTemplate().query("select * from account where id = ? ",
new BeanPropertyRowMapper<>(Account.class), id);
return accounts.isEmpty() ? null : accounts.get(0);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
public List<Account> getAll() {
try{
return super.getJdbcTemplate().query("select * from account",
new BeanPropertyRowMapper<>(Account.class));
}catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
public int updateAccount(Account account) {
try{
return super.getJdbcTemplate().update("update account set name=?,money=? where id=?",
account.getName(),
account.getMoney(),
account.getId());
}catch (Exception e) {
throw new RuntimeException(e);
}
}

@Override
public int deleteAccount(Integer id) {
try{
return super.getJdbcTemplate().update("delete from account where id=?", id);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
}

(2)XML配置文件spring-config.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- 配置数据访问层 -->
<bean id="accountDao" class="cn.frankfang.dao.impl.AccountDaoImpl">
<constructor-arg name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>

<!-- 配置业务层 -->
<bean id="accountService" class="cn.frankfang.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"/>
</bean>

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

<!-- 配置数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.cj.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/test?useAffectedRows=true"/>
<property name="user" value="root"/>
<property name="password" value="123456"/>
</bean>
</beans>

1、导入依赖

在项目的pom.xml文件中添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<dependencies>
<!-- spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<!-- spring tx -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<!-- spring jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<!-- spring aop -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
<!-- c3p0 datasource -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.5</version>
</dependency>
<!-- mysql driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
</dependencies>

2、添加约束

spring-config.xml文件中添加事务管理和AOP的约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 配置内容省略 -->

</bean>

3、配置事务管理器

spring-config.xml文件中添加以下内容:

1
2
3
4
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

4、配置事务的通知

spring-config.xml文件中添加以下内容:

1
2
3
4
<!-- 配置事务的通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 配置事务的属性 -->
</tx:advice>

其中id作为通知的唯一标识,transaction-manager用于指定容器中事务管理器的id

5、配置切入点表达式

spring-config.xml文件中添加以下内容:

1
2
3
4
<aop:config>
<!-- 配置切入点表达式 -->
<aop:pointcut id="pt1" expression="execution(public * cn.frankfang.service.impl.*.*(..))"/>
</aop:config>

6、建立事务通知和切入点表达式的对应关系

spring-config.xml文件中添加以下内容:

1
2
3
4
5
6
<aop:config>
<!-- 配置切入点表达式 -->
<aop:pointcut id="pt1" expression="execution(public * cn.frankfang.service.impl.*.*(..))"/>
<!-- 建立切入点表达式和事务通知之间的关系 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"/>
</aop:config>

其中advice-ref用于指定事务通知的idpointcut-ref用于指定切入点表达式的id

7、配置事务的属性

spring-config.xml文件中添加以下内容:

1
2
3
4
5
6
7
<!-- 配置事务的通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 配置事务的属性 -->
<tx:attributes>
<tx:method name="transfer" propagation="REQUIRED" read-only="false"/>
</tx:attributes>
</tx:advice>

下面将通过表格对<tx:method/>标签中的属性进行介绍:

属性 作用
name 用于指定需要进行事务管理的业务层方法名称,支持*通配符
isolation 用于指定事务的隔离级别,默认为ISOLATION_DEFAULT
propagation 用于指定事务的传播行为,默认为PROPAGATION_REQUIRED
read-only 用于指定事务是否只读,默认为false,只有查询方法才可设置为true
timeout 用于指定事务的超时时间,默认值为-1
rollback-for 用于指定一个异常,当产生该异常时事务回滚,产生其它异常时事务不回滚,没有指定则表示所有异常都回滚
no-rollback-for 用于指定一个异常,当产生该异常时事务不回滚,产生其它异常时事务不回滚,没有指定则表示所有异常都回滚

在配置完事务的属性之后就可以进行测试,检测是否达到了预计的效果。

三、基于注解的事务控制

除了可以使用XML声明式的方式进行事务控制,还可以通过注解的方法实现事务控制,下面将接着上面的例子来介绍如何基于注解实现事务控制。

1、配置事务管理器

如果采用XML+注解混合模式的话,与上文通过XML声明式的方式相同,这里不再赘述;如果使用纯注解方式,需要在配置类中添加以下内容:

1
2
3
4
5
6
7
8
9
/**
* 用于创建事务管理器对象
* @param dataSource 数据源
* @return 事务管理器
*/
@Bean("transactionManager")
public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}

2、开启Spring对注解事务的支持

如果采用XML+注解混合模式的话,需要在spring-config.xml文件中添加以下内容:

1
2
<!-- 开启Spring对注解事务的支持 -->
<tx:annotation-driven transaction-manager="transactionManager"/>

如果使用纯注解方式,只需在配置类上加上@EnableTransactionManagement注解即可

3、使用@Transactional注解

在需要进行事务控制的类或方法上添加@Transactional注解即可实现事务控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
@Override
public boolean transfer(Integer sourceAccountId, Integer targetAccountId, Double money) {
// 1.根据id查询两个账户信息
Account sourceAccount = accountDao.selectById(sourceAccountId);
Account targetAccount = accountDao.selectById(targetAccountId);
// 2.判断要转出的账户余额是否充足
if (sourceAccount.getMoney() < money) {
// 余额不足,转账失败
return false;
}
// 余额足够,继续转账
sourceAccount.setMoney(sourceAccount.getMoney() - money);
targetAccount.setMoney(targetAccount.getMoney() + money);

// 3.将修改的信息写入数据库
if (accountDao.updateAccount(sourceAccount) == 0) {
return false;
}

// 模拟转账异常
int i = 1 / 0;

// 4.将修改的信息写入数据库
if (accountDao.updateAccount(targetAccount) == 0) {
return false;
}
return true;
}

@Transactional注解的属性与<tx:method/>标签中的属性基本相同,这里不再进行赘述。

在添加完相关注解之后就可以进行测试,检测是否达到了预计的效果。

四、总结

Spring框架为我们提供了事务控制的功能,可通过XML文件或注解来配置Spring中的事务控制,在实际开发中可根据需要来选择配置的方式,若需获取更多关于Spring事务控制的相关内容,请参阅:Transaction Management