- Ubuntu 22.04
- IntelliJ IDEA 2022.1.3
- JDK 17.0.3
- Spring 5.3.21
- MyBatis 3.5.10
创建Maven项目 test0703 。
修改 pom.xml ,添加依赖以及设置静态资源:
......
junit
junit
4.13.2
test
org.mybatis
mybatis
3.5.10
mysql
mysql-connector-java
8.0.29
org.springframework
spring-webmvc
5.3.21
org.mybatis
mybatis-spring
2.0.7
org.springframework
spring-jdbc
5.3.21
src/main/java
***.xml
false
......
创建POJO MyObject.java :
package pojo;
public class MyObject {
private int c1;
private int c2;
public int getC1() {
return c1;
}
public void setC1(int c1) {
this.c1 = c1;
}
public int getC2() {
return c2;
}
public void setC2(int c2) {
this.c2 = c2;
}
@Override
public String toString() {
return "MyObject{" +
"c1=" + c1 +
", c2=" + c2 +
'}';
}
}
创建Mapper MyMapper.java 和 MyMapper.xml :
package dao;
import pojo.MyObject;
import java.util.List;
public interface MyMapper {
public List read();
public void add(int c1, int value);
public void reduce(int c1, int value);
}
select c1, c2 from t1 update t1 set c2 = c2 + #{param2} where c1 = #{param1} update t1 set c2 = c2 - #{param2} where c1 = #{param1}
创建Service MyService.java (接口)和 MyServiceImpl1.java (实现类):
package service;
public interface MyService {
public void show();
public void change();
}
package service;
import dao.MyMapper;
import pojo.MyObject;
import java.util.List;
public class MyServiceImpl1 implements MyService{
private MyMapper mapper;
public void setMyMapper(MyMapper mapper) {
this.mapper = mapper;
}
@Override
public void show() {
List list = mapper.read();
list.forEach(System.out::println);
}
@Override
public void change() {
int value = 100;
try {
mapper.reduce(1, value);
mapper.add(2, value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
创建 applicationContext.xml (在 resources 目录):
创建测试 MyTest.java (在 src/test/java 目录):
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import service.MyService;
import java.io.IOException;
public class MyTest {
@Test
public void test1() throws IOException {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyService service = context.getBean("myService1", MyService.class);
System.out.println("==================== Before: ====================");
service.show();
service.change();
System.out.println("==================== After: ====================");
service.show();
}
}
运行测试,如下:
==================== Before: ====================
MyObject{c1=1, c2=10000}
MyObject{c1=2, c2=10000}
==================== After: ====================
MyObject{c1=1, c2=9900}
MyObject{c1=2, c2=10100}
本例模拟了一个简单的转账操作, c1 账户转出100元,同时 c2 账户转入100元。
没有事务管理的问题假定转入操作失败了。本例中,为了模拟操作失败,我们故意把SQL写错:
......
updateaaa t1 set c2 = c2 + #{param2} where c1 = #{param1}
......
再次运行测试:
==================== Before: ====================
MyObject{c1=1, c2=9900}
MyObject{c1=2, c2=10100}
org.springframework.jdbc.BadSqlGrammarException:
### Error updating database. Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'updateaaa t1 set c2 = c2 + 100 where c1 = 2' at line 1
### The error may exist in file [/home/ding/IdeaProjects/test0703/target/classes/dao/MyMapper.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: updateaaa t1 set c2 = c2 + ? where c1 = ?
### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'updateaaa t1 set c2 = c2 + 100 where c1 = 2'
......
==================== After: ====================
MyObject{c1=1, c2=9800}
MyObject{c1=2, c2=10100}
可见,这样就会出现问题, c1 账户已经转出100元,而 c2账户转入100元的操作失败了。
转账是一个典型的事务处理操作,应该使用事务管理,如果某一个操作失败,应该把整个事务回滚。
Spring的事务管理 声明式事务Spring的声明式事务基于AOP实现,它的优点是只需修改Spring配置文件,无需修改Service组件的代码逻辑。
spring-webmvc 已经包含了对Spring AOP的依赖,但是我们还需要添加对 aspectjweaver 的依赖:
......
org.aspectj
aspectjweaver
1.9.9.1
runtime
......
修改 applicationContext.xml 配置文件:
......
......
......
- 无论是声明式事务,还是编程式事务,都要添加 transactionManager ,格式是固定的,照抄就行(对于所需的 dataSource ,本例中使用的是构造器注入,如果换成属性注入也行);
- txAdvice 的格式基本也是固定的:
- 对于 method 属性,本例中使用了 name="*" ,表示所有方法都适用,可以改成比如 name="get*" ,则表示适用于 get 开头的方法;
- propagation="REQUIRED" 是默认属性,不写也可以,表示如果当前没有事务,就新建一个事务,这是最常见的选择;
- 此外,还有 read-only , tmeout 等属性,基本上顾名思义;
- myPointcut :expression="execution(* service.*.*(..))" 表示 service 包下的所有类的所有方法,参数不限(本例中其实只有 change() 方法需要做事务管理);
现在就可以运行测试了,但有一点需要注意,虽然说声明式事务是非侵入式的,不用修改任何代码就能直接运行,但是在本例中,service里使用了 try 块:
......
try {
mapper.reduce(1, value);
mapper.add(2, value);
} catch (Exception e) {
e.printStackTrace();
}
......
这会对事务失败的判断产生影响。具体说来,因为代码中捕获并处理了异常,事务管理器就不会认为事务失败并回滚事务了,就跟没有事务管理的效果一样,所以这里一定要把 try 去掉:
// try {
mapper.reduce(1, value);
mapper.add(2, value);
// } catch (Exception e) {
// e.printStackTrace();
// }
为了使得异常之后的代码能继续运行,可以把 try 块放到测试里:
......
try {
service.change();
} catch (Exception e) {
e.printStackTrace();
}
......
把两条记录的 c2 值都还原为 10000 ,重新运行测试,如下:
==================== Before: ====================
MyObject{c1=1, c2=10000}
MyObject{c1=2, c2=10000}
org.springframework.jdbc.BadSqlGrammarException:
### Error updating database. Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'updateaaa t1 set c2 = c2 + 100 where c1 = 2' at line 1
### The error may exist in file [/home/ding/IdeaProjects/test0703/target/classes/dao/MyMapper.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: updateaaa t1 set c2 = c2 + ? where c1 = ?
### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'updateaaa t1 set c2 = c2 + 100 where c1 = 2'
......
==================== After: ====================
MyObject{c1=1, c2=10000}
MyObject{c1=2, c2=10000}
可见,出现异常后,事务回滚了。具体来说,因为转入操作失败,则之前转出的操作回滚了。
编程式事务修改 applicationContext.xml 文件:
......
......
- transactionManager :和声明式事务一样,需要配置 transactionManager ;
创建 MyServiceImpl2.java :
package service;
import dao.MyMapper;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import pojo.MyObject;
import java.util.List;
public class MyServiceImpl2 implements MyService{
private final PlatformTransactionManager transactionManager;
private MyMapper mapper;
public MyServiceImpl2(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setMyMapper(MyMapper mapper) {
this.mapper = mapper;
}
@Override
public void show() {
List list = mapper.read();
list.forEach(System.out::println);
}
@Override
public void change() {
TransactionStatus txStatus =
transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
int value = 100;
mapper.reduce(1, value);
System.out.println("===================================");
mapper.add(2, value);
} catch (Exception e) {
transactionManager.rollback(txStatus);
throw e;
}
transactionManager.commit(txStatus);
}
}
注:为了防止事务一开始就报错引起混淆,代码里加了一行输出,如果看到这行输出,说明转出操作已经做了。
注意:该方法需要显式 try ,捕获异常,并回滚事务。而前面介绍的声明式事务则不能捕获异常,要留给事务管理器来判断异常并回滚事务。
在 applicationContext.xml 中注册 MyServiceImpl2 :
......
......
创建测试如下:
@Test
public void test2() throws IOException {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyService service = context.getBean("myService2", MyService.class);
service.show();
try {
service.change();
} catch (Exception e) {
e.printStackTrace();
}
service.show();
}
把SQL改成正确写法,把两条记录的 c2 值都还原为 10000 ,运行test2,如下:
MyObject{c1=1, c2=10000}
MyObject{c1=2, c2=10000}
===================================
MyObject{c1=1, c2=9900}
MyObject{c1=2, c2=10100}
把SQL改成错误写法,把两条记录的 c2 值都还原为 10000 ,运行test2,如下:
MyObject{c1=1, c2=10000}
MyObject{c1=2, c2=10000}
===================================
org.springframework.jdbc.BadSqlGrammarException:
### Error updating database. Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'updateaaa t1 set c2 = c2 + 100 where c1 = 2' at line 1
### The error may exist in file [/home/ding/IdeaProjects/test0703/target/classes/dao/MyMapper.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: updateaaa t1 set c2 = c2 + ? where c1 = ?
### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'updateaaa t1 set c2 = c2 + 100 where c1 = 2'
......
MyObject{c1=1, c2=10000}
MyObject{c1=2, c2=10000}
可见,转入操作失败,事务回滚了。
编程式事务有一个变种的用法:
......
@Override
public void change() {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.execute(txStatus -> {
int value = 100;
mapper.reduce(1, value);
System.out.println("===================================");
mapper.add(2, value);
return null;
});
}
......
效果和前面一样,不再赘述。
注意:这里跟声明式事务类似,千万不要 try 并且捕获异常,否则事务管理器就不会认为事务失败了。
还有一点需要注意的是,在配置 SqlSessionFactory 时,不要添加如下属性:
否则会报错: org.springframework.dao.TransientDataAccessResourceException: SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization
该属性值表示由容器管理事务,而不使用Spring的事务管理。
总结- 声明式事务使用AOP来管理事务,用法相对比较简单,而且是非侵入式的,推荐使用;
- 编程式事务需要在代码逻辑里显式管理事务,相对比较麻烦;
- 对于声明式事务以及编程式事务的变种写法,千万记得不要 try 并且捕获异常,否则事务管理器就不会认为事务失败了。当然,可以在外层捕获异常;
- http://mybatis.org/spring
- http://mybatis.org/spring/zh/index.html (中文版)
- https://www.bilibili.com/video/BV1WE411d7Dv?p=27&vd_source=2ec1bf89a54b0d3afd52d89760b0dd55



