背景

在做业务开发时,遇到了一个事务不起作用的问题。大概流程是这样的,方法内部的方法调用了一个带事务的方法,失败后事务没有回滚。查阅资料后,问题得到解决,记录下来分享给大家。

场景

在一个UserService里面,一个内部方法callSaveUser调用该service里面的saveUser方法 

@Override
  @Transactional(rollbackFor = RuntimeException.class)
  public void saveUser(User user) {
    userMapper.save(user);
    throw new RuntimeException("");
  }
 
  /**
   * 内部调用保存用户方法
   *
   * @param user
   */
  @Override
  public void callSaveUser(User user) {
    this.saveUser(user);
  }

原因

AOP使用的是动态代理的机制,它会给类生成一个代理类,事务的相关操作都在代理类上完成。内部方式使用this调用方式时,使用的是实例调用,并没有通过代理类调用方法,所以会导致事务失效。

过程如下图:

spring子方法不加入事务 spring事务不起作用_事务


首先调用的是AOP代理对象而不是目标对象,首先执行事务切面,事务切面内部通过TransactionInterceptor环绕增强进行事务的增强,即进入目标方法之前开启事务,退出目标方法时提交/回滚事务。

解决办法

1. 通过初始化方法在目标对象中注入代理对象

注入自身bean

@Autowired
@Lazy
private UserService service;
修改方法调用

/**
   * 解决方法一 在bean中将自己注入进来
   * @param user
   */
  @Override
  public void callSaveUser(User user) {
    this.service.saveUser(user);
  }

总结:只能解决 普通(无循环依赖)的 的Bean注入AOP代理,无法解决循环依赖的AOP代理对象注入问题,即无法解决目标对象的自我调用问题。

2. 通过ApplicationContext引入bean

注入ApplicationContext

@Autowired
ApplicationContext applicationContext;
修改方法调用

/**
   * 解决方法二 通过applicationContext获取到bean
   * @param user
   */
  @Override
  public void callSaveUser(User user) {
    ((UserService)applicationContext.getBean("userService")).saveUser(user);
  }

总结:能解决普通(无循环依赖)的AOP代理对象注入问题,而且也能解决循环依赖(应该是singleton之间的循环依赖)造成的目标对象无法注入AOP代理对象问题,但该解决方案不适合解决循环依赖中包含prototype Bean的自我调用问题。

3. 通过AopContext获取当前类的代理类(推荐)

通过AopContext获取当前类的代理类,直接通过代理类调用方法在引导类上添加@EnableAspectJAutoProxy(exposeProxy=true)注解,然后修改callSaveUser方法

/**
   * 解决方法三 通过applicationContext获取到bean
   *
   * @param user
   */
  @Override
  public void invokeInsertUser(User user) {
    ((UserService) AopContext.currentProxy()).callSaveUser(user);
  }

原理:

  1. 在进入代理对象之后通过AopContext.serCurrentProxy(proxy)暴露当前代理对象ThreadLocal,并保存上次ThreadLocal绑定的代理对象为oldProxy;
  2. 接下来我们可以通过 AopContext.currentProxy() 获取当前代理对象;
  3. 在退出代理对象之前要重新将ThreadLocal绑定的代理对象设置为上一次的代理对象,即AopContext.serCurrentProxy(oldProxy)。

有些人不喜欢这种方式,说通过ThreadLocal暴露有性能问题,其实这个不需要考虑,因为事务相关的(Session和Connection)内部也是通过SessionHolder和ConnectionHolder暴露ThreadLocal实现的。