什么场景下声明式事务会失效?如何解决?

先说下使用的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注解将会失效,因为这是目标对象内部的自调用,没有经过代理对象,所以无法进行增强。

解决办法大体有两种,一种是依赖注入,即自注入;另一种是依赖查找,自己查找自己。推荐使用依赖注入,因为每次都进行依赖查找会有一些性能上的开销。

你可能感兴趣的:(Spring,Context,Spring,声明式事务,数据库,java,spring,mysql,mybatis)