主要讲解注解@Transactional的基础知识、使用姿势,以及事务不生效的几种Case。
前面已经讲解了IOC的基础知识,以及Spring常用的注解,这篇文章是对上一篇文章《【Spring基础系列3】Spring常用的注解》的补充,由于这个注解需要讲述的内容比较多,一方面该注解非常重要,另一方面非常容易入坑,所以这个注解的内容,就单独放到这篇文章来讲。
为了更好通过示例讲解注解@Transactional的特性,本文会有大量的示例,这些示例是依赖如下配置和数据,如果只关注基础知识,可以跳过这一部分。
使用的是Mysql + Innodb存储引擎,事务隔离级别设置为可重复读RR。
pom.xml需要添加的依赖包:
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
使用Spring + MyBatis的方式对DB进行操作,下面是XML映射文件:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mybatis.dao.UserDao">
<!-- 根据uid查询一个用户信息 -->
<select id="selectUserById" parameterType="Integer" resultType="com.mybatis.entity.MyUser">
select * from user_test where uid = #{uid}
</select>
<!--修改一个用户 -->
<update id="updateUser" parameterType="com.mybatis.entity.MyUser">
update user_test set uname =#{uname},usex = #{usex} where uid = #{uid}
</update>
</mapper>
提供的接口:
@Repository("userDao")
@Mapper
/*
* 使用Spring自动扫描MyBatis的接口并装配 (Spring将指定包中所有被@Mapper注解标注的接口自动装配为MyBatis的映射接口
*/
public interface UserDao {
/**
* 接口方法对应的SQL映射文件中的id
*/
public MyUser selectUserById(Integer uid);
public int updateUser(MyUser user);
}
DB结构:
CREATE TABLE `user_test` (
`uid` tinyint(2) NOT NULL,
`uname` varchar(20) DEFAULT NULL,
`usex` varchar(10) DEFAULT NULL,
PRIMARY KEY (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
DB初始数据:
| uid | uname | usex |
|---|---|---|
| 1 | 张三 | 女 |
| 2 | 陈恒 | 男 |
| 3 | 楼仔 | 男 |
讲解基础知识前,我们先看@Transactional怎么使用,下面是DB数据正常更新的情况:
@Controller("userController")
public class UserController {
@Autowired
private UserDao userDao;
public void update(Integer id) {
MyUser user = new MyUser();
user.setUid(id);
user.setUname("张三-testing"); // 变更数据
user.setUsex("女");
userDao.updateUser(user);
}
public MyUser query(Integer id) {
MyUser user = userDao.selectUserById(id);
return user;
}
@Transactional(rollbackFor = Exception.class)
public void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原记录:" + user);
update(id);
//throw new Exception("测试事务回滚生效");
}
}
再看一下测试用例:
public static void main(String[] args) throws Exception {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserController uc = (UserController) applicationContext.getBean("userController");
try {
uc.testSuccess();
} finally {
MyUser user = uc.query(1);
System.out.println("修改后的记录:" + user);
}
}
// 输出:
// 原记录:User[uid=1,uname=张三,usex=女]
// 修改后的记录:User[uid=1,uname=张三-test,usex=女]
我们发现DB更新成功,现在我们修改一下代码,将DB数据回退到原始状态,然后再把抛出异常放开,看事务是否生效:
public void update(Integer id) {
MyUser user = new MyUser();
user.setUid(id);
user.setUname("张三-testing"); // 变更数据
user.setUsex("女");
userDao.updateUser(user);
}
@Transactional(rollbackFor = Exception.class)
public void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原记录:" + user);
update(id);
throw new Exception("测试事务回滚生效");
}
再看看执行结果:
原记录:User[uid=1,uname=张三,usex=女]
修改后的记录:User[uid=1,uname=张三,usex=女]
我们发现因为程序抛出异常,DB数据正常回滚,符合预期。
pring所有的事务管理策略类都继承自org.springframework.transaction.PlatformTransactionManager接口:
public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
事务的隔离级别:
事务传播行为(如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为):
字段说明:
所有的测试Case,测试完毕后,DB数据需要手动更新成原始数据,保证测试Case互不影响。
简单来讲就是指非直接访问带注解标记的方法 B,而是通过类普通方法 A,然后由 A 访问 B,下面是一个简单的 case,我们在类UserController中新增一个方法testFail():
public void testFail() throws Exception {
testSuccess();
throw new Exception("测试事务回滚不生效");
}
这里我们是通过testFail()调用testSuccess(),再看一下测试用例:
public static void main(String[] args) throws Exception {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserController uc = (UserController) applicationContext.getBean("userController");
try {
uc.testFail();
} finally {
MyUser user = uc.query(1);
System.out.println("修改后的记录:" + user);
}
}
// 输出:
// 原记录:User[uid=1,uname=张三,usex=女]
// 修改后的记录:User[uid=1,uname=张三,usex=女]
从上面的输出可以看到,事务并没有回滚,主要是因为类内部调用,不会通过代理方式访问。
在私有方法上,添加@Transactional注解也不会生效,私有方法外部不能访问,所以只能内部访问,上面的 case 不生效,这个当然也不生效了:
@Transactional(rollbackFor = Exception.class)
private void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原记录:" + user);
update(id);
throw new Exception("测试事务生效");
}
直接使用时,下面这种场景不太容易出现,因为 IDEA 会有提醒,文案为: Methods annotated with '@Transactional' must be overridable
@Transactional注解默认处理运行时异常,即只有抛出运行时异常时,才会触发事务回滚,否则并不会回滚:
@Transactional
public void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原记录:" + user);
update(id);
throw new Exception("测试事务生效");
}
测试 case 如下:
public static void main(String[] args) throws Exception {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserController uc = (UserController) applicationContext.getBean("userController");
try {
uc.testSuccess();
} finally {
MyUser user = uc.query(1);
System.out.println("修改后的记录:" + user);
}
}
// 输出:
// 原记录:User[uid=1,uname=张三,usex=女]
// 修改后的记录:User[uid=1,uname=张三-test,usex=女]
输出结果如下,事务并未回滚(如果需要解决这个问题,通过设置@Transactional的 rollbackFor 属性即可)
这个场景可能并不多见,在标记事务的方法内部,另起子线程执行 db 操作,此时事务同样不会生效
下面给出两个不同的姿势,一个是子线程抛异常,主线程 ok;一个是子线程 ok,主线程抛异常。
我们在类UserController中新增一个方法testMultThread(),该方法在主线程会抛出异常:
public void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原记录:" + user);
update(id);
}
@Transactional(rollbackFor = Exception.class)
public void testMultThread() throws Exception {
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
testSuccess();
}
}).start();
throw new Exception("测试事务不生效");
}
上面这种场景不生效很好理解,子线程的异常不会被外部的线程捕获,testMultThread这个方法的调用不抛异常,因此不会触发事务回滚,调用方式和前面一样,就换一个方法:
uc.testMultThread();
这里提醒一下,输出的数据因为都是在主线程中输出,所以输出结果都是“张三”,后来我看了库表,发现DB数据已经更新为“张三-testing”,所以事务回滚没有生效。
我们修改代码如下,让子任务抛出异常:
public void testSuccess() throws Exception {
Integer id = 1;
MyUser user = query(id);
System.out.println("原记录:" + user);
update(id);
throw new Exception("测试事务生效");
}
@Transactional(rollbackFor = Exception.class)
public void testMultThread() throws Exception {
new Thread(new Runnable() {
@SneakyThrows
@Override
public void run() {
testSuccess();
}
}).start();
}
同上,DB数据没有回滚,发现DB数据已经更新为“张三-testing”,所以事务回滚没有生效。
这个内容,我后面再单独出一篇文章来讲,要不然这篇文章篇幅又太长了。
感觉@Transactional这个注解需要了解的内容稍微有点多,不过这个注解是我们绕不开的,所以需要大家好好学习,特别是事务不生效的坑!其中有一个坑“类内部访问”,我刚开始写JAVA是就踩过,后来还是同事告诉我的,现在回顾当时的状态,感觉真的是菜的不能再菜了。
文章后面遗留了“传播属性”的坑,这个我就先放一下,等后面有时间我再写,然后关于Spring的部分,还剩下AOP,这几天我会先把相关知识看完,然后再通过文章的形式分享出来,敬请期待...
欢迎大家多多点赞,更多文章,请关注微信公众号“楼仔进阶之路”,点关注,不迷路~~