基于AOP的声明式事务控制
# 1、Spring事务编程概述
事务是开发中必不可少的东西,使用JDBC开发时,我们使用connnection对事务进行控制,使用MyBatis时,我们使用SqlSession对事务进行控制,缺点显而易见,当我们切换数据库访问技术时,事务控制的方式总会变化,Spring 就将这些技术基础上,提供了统一的控制事务的接口。Spring的事务分为:编程式事务控制 和 声明式事务控制
| 事务控制方式 | 解释 |
|---|---|
| 编程式事务控制 | Spring 提供了事务控制的类和方法,使用编码的方式对业务代码进行事务控制,事务控制代码和业务操作代码耦合到了一起,开发中不使用。 |
| 声明式事务控制 | Spring 将事务控制的代码封装,对外提供了 XML 和注解配置方式,通过配置的方式完成事务的控制,可以实现事务控制与业务代码解耦,开发中推荐使用。 |
Spring事务编程相关的类主要有如下三个
| 事务控制相关类 | 解释 |
|---|---|
| 平台事务管理器<br>PlatformTransactionManager | 是一个接口标准,实现类都具备事务提交、回滚和获得事务对象的功能,不同持久层框架可能会有不同实现方案。 |
| 事务定义<br>TransactionDefinition | 封装事务的隔离级别、传播行为、过期时间等属性信息。 |
| 事务状态<br>TransactionStatus | 存储当前事务的状态信息,包括事务是否已提交、是否回滚、是否存在回滚点等。 |
虽然编程式事务控制我们不学习,但是编程式事务控制对应的这些类我们需要了解一下,因为我们在通过配置的方式进行声明式事务控制时也会看到这些类的影子
# 2、搭建测试环境
搭建一个转账的环境,dao层一个转出钱的方法,一个转入钱的方法,service层一个转账业务方法,内部分别调用dao层转出钱和转入钱的方法,准备工作如下:
数据库准备一个账户表tb_account;
dao层准备一个AccountMapper,包括incrMoney和decrMoney两个方法;
service层准备一个transferMoney方法,分别调用incrMoney和decrMoney方法;
在applicationContext文件中进行Bean的管理配置;
测试正常转账与异常转账。
# 3、基于xml声明式事务控制
结合上面我们学习的AOP的技术,很容易就可以想到,可以使用AOP对Service的方法进行事务的增强。
目标类:AccountServiceImpl
切点:service业务类中的所有业务方法
通知类:Spring提供的,通知方法已经定义好,只需要配置即可
我们分析:
目标类是我们自己定义的;
通知类是Spring提供的事务增强,且内部的通知方法是固定的,所以结合上面我们学习的AOP的技术,很容易就可以想到,可以使用AOP对Service的方法进行事务的增强。
目标类:自定义的AccountServiceImpl,内部的方法是切点
通知类:Spring提供的,通知方法已经定义好,只需要配置即可
我们分析:
通知类是Spring提供的,需要导入Spring事务的相关的坐标;
配置目标类AccountServiceImpl;
使用advisor标签配置切面。
导入Spring事务的相关的坐标,spring-jdbc坐标已经引入的spring-tx坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.13.RELEASE</version>
</dependency>

配置目标类AccountServiceImpl
<bean id="accountService" class="com.itheima.service.impl.AccoutServiceImpl">
<property name="accountMapper" ref="accountMapper"></property>
</bean>
使用advisor标签配置切面
<aop:config>
<aop:advisor advice-ref="Spring提供的通知类" pointcut="execution(* com.itheima.service.impl.*.*(..))"/>
</aop:config>
疑问:Spring提供的通知类是谁?是谁?是spring-tx包下的advice标签配置提供的
xmlns:tx="http://www.springframework.org/schema/tx"
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/springtx.xsd
<!-- 配置 Spring 提供的事务通知 -->
<tx:advice id="myAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 配置需要事务控制的方法(可配置多个) -->
<tx:method name="transferMoney"/>
</tx:attributes>
</tx:advice>
<!-- 配置平台事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<aop:config>
<aop:advisor advice-ref="myAdvice" pointcut="execution(* com.itheima.service.impl.*.*(..))"/>
</aop:config>
对上述配置进行详解一下
首先,平台事务管理器PlatformTransactionManager是Spring提供的封装事务具体操作的规范接口,封装了事务的提交和回滚方法
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
void commit(TransactionStatus var1) throws TransactionException;
void rollback(TransactionStatus var1) throws TransactionException;
}
不同的持久层框架事务操作的方式有可能不同,所以不同的持久层框架有可能会有不同的平台事务管理器实现,例如,MyBatis作为持久层框架时,使用的平台事务管理器实现是DataSourceTransactionManager。Hibernate作为持久层框架时,使用的平台事务管理器是HibernateTransactionManager。
其次,事务定义信息配置,每个事务有很多特性,例如:隔离级别、只读状态、超时时间等,这些信息在开发时可以通过connection进行指定,而此处要通过配置文件进行配置
<tx:attributes>
<tx:method name="方法名称"
isolation="隔离级别"
propagation="传播行为"
read-only="只读状态"
timeout="超时时间"/>
</tx:attributes>
其中,name属性名称指定哪个方法要进行哪些事务的属性配置,此处需要区分的是切点表达式指定的方法与此处指定的方法的区别?切点表达式,是过滤哪些方法可以进行事务增强;事务属性信息的name,是指定哪个方法要进行哪些事务属性的配置

方法名在配置时,也可以使用 * 进行模糊匹配,例如:
<!-- 配置 Spring 提供的事务通知 -->
<tx:advice id="myAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 精确匹配 transferMoney 方法 -->
<tx:method name="transferMoney"/>
<!-- 模糊匹配所有以 Service 结尾的方法 -->
<tx:method name="*Service"/>
<!-- 模糊匹配所有以 insert 开头的方法 -->
<tx:method name="insert*"/>
<!-- 模糊匹配所有以 update 开头的方法 -->
<tx:method name="update*"/>
<!-- 模糊匹配任意方法,通常作为兜底匹配放在最后 -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
isolation属性:指定事务的隔离级别,事务并发存在三大问题:脏读、不可重复读、幻读/虚读。可以通过设置事务的隔离级别来保证并发问题的出现,常用的是READ_COMMITTED 和 REPEATABLE_READ
| 隔离级别(isolation) | 解释 |
|---|---|
| DEFAULT | 默认隔离级别,取决于当前数据库的设置。例如 MySQL 默认是 REPEATABLE_READ。 |
| READ_UNCOMMITTED | A 事务可以读取到 B 事务尚未提交的数据,不能解决任何并发问题,安全性最低,性能最高。 |
| READ_COMMITTED | A 事务只能读取到其他事务已提交的数据,可以解决脏读,但不能解决不可重复读和幻读问题。 |
| REPEATABLE_READ | A 事务多次读取同一数据结果一致,可以解决不可重复读,但不能完全避免幻读(MySQL 做了优化)。 |
| SERIALIZABLE | 串行化执行事务,可解决所有并发问题,安全性最高,性能最低,几乎不适用于高并发场景。 |
read-only属性:设置当前的只读状态,如果是查询则设置为true,可以提高查询性能,如果是更新(增删改)操作则设置为false
<!-- 一般查询相关的业务操作都会设置为只读模式 -->
<tx:method name="select*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
timeout属性:设置事务执行的超时时间,单位是秒,如果超过该时间限制但事务还没有完成,则自动回滚事务,不在继续执行。默认值是-1,即没有超时时间限制
<!-- 设置查询操作的超时时间是3秒 -->
<tx:method name="select*" read-only="true" timeout="3"/>
propagation属性:设置事务的传播行为,主要解决是A方法调用B方法时,事务的传播方式问题的,例如:使用单方的事务,还是A和B都使用自己的事务等。事务的传播行为有如下七种属性值可配置
| 事务传播行为 | 解释 |
|---|---|
| REQUIRED(默认值) | A 调用 B,B 需要事务:如果 A 有事务,B 加入 A 的事务;如果 A 没有事务,B 就新建一个事务。 |
| REQUIRES_NEW | A 调用 B,B 总是开启一个新事务:如果 A 有事务则挂起,B 启动一个新的事务。 |
| SUPPORTS | A 调用 B,B 支持事务:如果 A 有事务,B 加入 A 的事务;如果 A 没有事务,B 就非事务方式执行。 |
| NOT_SUPPORTED | A 调用 B,B 不支持事务:如果 A 有事务,则挂起 A 的事务,B 以非事务方式执行。 |
| NEVER | A 调用 B,B 不允许有事务:如果 A 有事务则抛出异常,否则以非事务方式执行。 |
| MANDATORY | A 调用 B,B 要求必须在事务中执行:如果 A 有事务,B 加入;如果 A 没有事务则抛出异常。 |
| NESTED | A 调用 B,B 启动一个嵌套事务:如果 A 有事务,B 作为 A 的子事务;如果 A 没有事务,B 启动一个新事务。 |
xml方式声明式事务控制的原理浅析一下
<tx:advice>标签使用的命名空间处理器是TxNamespaceHandler,内部注册的是解析器是TxAdviceBeanDefinitionParser
this.registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser());
TxAdviceBeanDefinitionParser中指定了要注册的BeanDefinition
protected Class<?> getBeanClass(Element element) {
return TransactionInterceptor.class;
}
TxAdviceBeanDefinitionParser二级父类AbstractBeanDefinitionParser的parse方法将TransactionInterceptor以配置的名称注册到了Spring容器中
parserContext.registerComponent(componentDefinition);
TransactionInterceptor中的invoke方法会被执行,跟踪invoke方法,最终会看到事务的开启和提交
在AbstractPlatformTransactionManager的132行中开启的事务;
在TransactionAspectSupport的242行提交了事务。
# 4、基于注解声明式事务控制
注解就是对xml的替代
@Service("accountService")
public class AccoutServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
// <tx:method name="*" isolation="REPEATABLE_READ" propagation="REQUIRED"/>
@Transactional(
isolation = Isolation.REPEATABLE_READ,
propagation = Propagation.REQUIRED,
readOnly = false,
timeout = 5
)
public void transferMoney(String decrAccountName, String incrAccountName, int money) {
accountMapper.decrMoney(decrAccountName, money); // 转出钱
int i = 1 / 0; // 模拟某些逻辑产生的异常
accountMapper.incrMoney(incrAccountName, money); // 转入钱
}
}
同样,使用的事务的注解,平台事务管理器仍然需要配置,还需要进行事务注解开关的开启
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置事务的注解驱动-->
<tx:annotation-driven transaction-manager="transactionManager"/>
如果使用全注解的话,使用如下配置类的形式代替配置文件
@Configuration
@ComponentScan("com.itheima.service")
@PropertySource("classpath:jdbc.properties")
@MapperScan("com.itheima.mapper")
@EnableTransactionManagement
public class ApplicationContextConfig {
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
// ... 省略其他配置 ...
}