背景
在做业务开发时,遇到了一个事务不起作用的问题。大概流程是这样的,方法内部的方法调用了一个带事务的方法,失败后事务没有回滚。查阅资料后,问题得到解决,记录下来分享给大家。
场景
在一个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调用方式时,使用的是实例调用,并没有通过代理类调用方法,所以会导致事务失效。
过程如下图:
首先调用的是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);
}
原理:
- 在进入代理对象之后通过AopContext.serCurrentProxy(proxy)暴露当前代理对象ThreadLocal,并保存上次ThreadLocal绑定的代理对象为oldProxy;
- 接下来我们可以通过 AopContext.currentProxy() 获取当前代理对象;
- 在退出代理对象之前要重新将ThreadLocal绑定的代理对象设置为上一次的代理对象,即AopContext.serCurrentProxy(oldProxy)。
有些人不喜欢这种方式,说通过ThreadLocal暴露有性能问题,其实这个不需要考虑,因为事务相关的(Session和Connection)内部也是通过SessionHolder和ConnectionHolder暴露ThreadLocal实现的。