先说下使用的ORM框架-MyBatis,数据库链接池为阿里巴巴的Druid。
Talk is cheap. Show me the code
数据库DDL
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_name` varchar(30) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8;
Java DO
@Data
public class TUserDO {
private Integer id;
private String userName;
}
Mapper
public interface TUserMapper {
@Select("SELECT * FROM t_user")
List<TUserDO> selectAll();
@Insert("INSERT INTO t_user VALUES(null,#{userName})")
Integer insert(TUserDO userDO);
}
启动类
为了演示方便,这里把引导和业务写在了一起。
@Configuration // 标明当前类是一个配置类,不加也可以
@EnableTransactionManagement // 声明启用Spring声明式事务
@MapperScan("com.xxx.spring.dao") // 指定Mapper接口存放的包路径
public class TransactionalDemo {
@Autowired
private TUserMapper userMapper;
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(TransactionalDemo.class);
context.refresh();
TransactionalDemo transactionalDemo = context.getBean(TransactionalDemo.class);
transactionalDemo.display();
System.out.println("---------------insert方法开始执行-------------------");
try{
transactionalDemo.insert();
}catch (Exception e){
// ignore exception
}
System.out.println("---------------insert方法执行之后-------------------");
transactionalDemo.display();
System.out.println("---------------test方法开始执行-------------------");
try{
transactionalDemo.test();
}catch (Exception e){
// ignore exception
}
System.out.println("---------------test方法执行之后-------------------");
transactionalDemo.display();
}
// 该方法就是用来查询t_user表中所有数据
public void display(){
List<TUserDO> tUserDOS = userMapper.selectAll();
System.out.println(tUserDOS);
}
// 向t_user表中插入一条数据
@Transactional(rollbackFor = Exception.class)
public void insert(){
TUserDO userDO = new TUserDO();
userDO.setUserName("王五");
userMapper.insert(userDO);
// 抛出异常
throw new RuntimeException();
}
// 在该方法中使用this来调用#insert方法
public void test(){
this.insert();
}
// 必须将sqlSessionFactory()方法定义为static修饰的,因为如果是非static修饰的,IoC容器
// 需要先实例化TransactionalDemo ,才能执行sqlSessionFactory()方法(底层基于反射来调用方 法),而实例化
// TransactionalDemo 就必须处理其依赖项->TUserMapper,而这时候IoC容器内部并没有
// TUserMapper接口的代理实例。因为MyBatis中的MapperFactoryBean在创建TUserMapper代理
// 实例时,其有一个依赖项就是SqlSessionFactory,IoC容器必须处理该依赖,这是就会出现循环引用错误。
@Bean
public static SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
factoryBean.afterPropertiesSet(); // 调不调用都可以,因为getObject方法会检查
return factoryBean.getObject();
}
@Bean
public static DataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/study?useSSL=false");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}
}
我们的期望的测试结果是数据库中t_user表一条记录都没有,因为在insert方法中抛出异常,Spring 声明式事务应该回滚掉。
ok,运行代码,t_user表中记录如下:
可以发现,与期望的结果并不符合,我们执行了两次insert方法,插入了一条数据(与期望不符),另一条是被回滚掉了(与期望相符),那么问题出在哪里呢?
@Transactional注解失效原因
其实问题根源就是在test方法中直接调用了insert方法,因为Spring的声明式事务是基于动态代理/字节码增强来完成的,虽然我们看上去调用的是TransactionalDemo实例的方法,但实际调用的是Spring 声明式事务所生成的TransactionalDemo实例的代理对象的方法,在这个代理对象中在事务方法(这里指的就是insert方法)执行前后开启事务和关闭事务,可以理解为Spring AOP中的环绕通知(@Around)。
如果直接在方法内部(这里指的就是test方法)调用当前类的添加事务的方法(这里指的就是insert方法),那么就相当于目标对象调用自己的方法,根本没有经过代理对象,从而导致Spring 声明式事务无法在目标方法的事务方法执行前后进行增强。所以@Transactional注解就失去了作用。
解决办法
依赖注入->自注入
调整代码如下:
// 声明一个TransactionalDemo依赖项
@Autowired
private TransactionalDemo transactionalDemo;
// 调整test方法逻辑
public void test(){
transactionalDemo.insert();
}
依赖查找
调整代码如下:
// 声明一个ApplicationContext依赖项
@Autowired
private ApplicationContext applicationContext;
// 调整test方法逻辑
public void test(){
applicationContext.getBean(TransactionalDemo.class).insert();
}
总结
Spring 声明式事务中的@Transactional注解会在自调用的场景下失效。失效的原因是:当我们在某个类中的方法上添加@Transactional注解时,声明式事务就会为当前类基于JDK动态代理或者字节码增强-CGLIB来创建一个代理对象。所以我们看上去调用的是目标对象,其实运行时执行的是代理对象,由代理对象在执行目标方法前后进行事务的开启和关闭。
而在目标对象方法中调用自己的其它事务方法,那么@Transactional注解将会失效,因为这是目标对象内部的自调用,没有经过代理对象,所以无法进行增强。
解决办法大体有两种,一种是依赖注入,即自注入;另一种是依赖查找,自己查找自己。推荐使用依赖注入,因为每次都进行依赖查找会有一些性能上的开销。