前些月在项目中碰到个关于事务的问题,今天抽空来整理了一波,简单分享下。
- 情况描述:大家在项目中应该都会碰到初始化某些数据的问题,有些会采用没有数据则插入的方式。
- 这个时候大家可能会很自然的在select方法中直接this.insert方法,咋一看由于默认的事务传播级别是PROPAGATION_REQUIRED(如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务),没有开启事务的listUser方法中调用开启了事务的insertUser方法是不会存在事务的问题。但是实际结果是insertUser方法中事务并没有开启。
- 这个时候大家可能会很自然的在select方法中直接this.insert方法,咋一看由于默认的事务传播级别是PROPAGATION_REQUIRED(如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务),没有开启事务的listUser方法中调用开启了事务的insertUser方法是不会存在事务的问题。但是实际结果是insertUser方法中事务并没有开启。
- 代码片段:
@RestController @RequestMapping("/user") public class UserController { @Resource private UserService userService; @GetMapping("/listUser") public void listUser() throws Exception { userService.listUser(); } @GetMapping("/insertDefault") public void insertDefault() throws Exception { User user = new User("XiaMu", "123456"); System.out.println(userService.hashCode()); userService.insertUser(user); } } @Service public class UserServiceImpl implements UserService { @Resource private UserDao userDao; @Override public void listUser() throws Exception { List<User> users = userDao.listUser(); if (users.isEmpty()) { User user = new User("XiaMu", "123456"); this.insertUser(user); } } @Override @Transactional public void insertUser(User user) throws Exception { userDao.insertUser(user); throw new RuntimeException(); } }复制代码
这个时候用postman调用/user/listUser,出现了异常但是并没有回滚
- 原因分析:
- spring中的事务是由aop实现的,spring中的aop又是由动态代理实现的,这个时候我们从这两方面去思考这个问题
- 将数据删除,在两个方法头部加上如下代码再次运行一次查看打印的信息
@Override
public void listUser() throws Exception {
// 得到上一个栈帧的信息(方法调用处)
StackTraceElement stack = (StackTraceElement)Thread.currentThread().getStackTrace()[2];
System.out.println("调用类名:" + stack.getClassName() + " 调用方法名:" + stack.getMethodName());
System.out.println("this:" + this.getClass());
List<User> users = userDao.listUser();
if (users.isEmpty()) {
User user = new User("XiaMu", "123456");
this.insertUser(user);
}
}
@Override
@Transactional
public void insertUser(User user) throws Exception {
// 得到上一个栈帧的信息(方法调用处)
StackTraceElement stack = (StackTraceElement)Thread.currentThread().getStackTrace()[2];
System.out.println("调用类:" + stack.getClassName() + " 调用方法名:" + stack.getMethodName());
userDao.insertUser(user);
throw new RuntimeException();
}复制代码
依旧没有事务,我们可以发现listUser的调用处是一个代理类的invoke方法。在listUser方法中打印的this是userServiceImpl对象,insertUser方法调用处正是userServiceImpl的方法。
调用类:com.xiamu.springboot.service.impl.UserServiceImpl$$FastClassBySpringCGLIB$$8ae4e401 调用方法名:invoke
this:class com.xiamu.springboot.service.impl.UserServiceImpl
调用类:com.xiamu.springboot.service.impl.UserServiceImpl 调用方法名:listUser复制代码
这个时候我们直接用postman调用/user/insertDefault,发现方法调用也是在代理类的invoke方法,并且事务生效了
调用类:com.xiamu.springboot.service.impl.UserServiceImpl$$FastClassBySpringCGLIB$$8ae4e401 调用方法名:invoke
复制代码
我们发现在这个代理类中调用insert方法时是有事务的,那么问题是不是在于list方法中的insert()并不是由代理类调用呢,再做次实验将list方法中的this.insertUser()改为下面这种方式
UserServiceImpl serviceImpl = SpringUtil.getBean("userServiceImpl");
serviceImpl.insertUser(user);复制代码
结果发现我们从spring中拿出来的userServiceImpl就是一个代理对象,并且此时有了事务并回滚了。
调用类名:com.xiamu.springboot.service.impl.UserServiceImpl$$FastClassBySpringCGLIB$$8ae4e401 调用方法名:invoke
this:class com.xiamu.springboot.service.impl.UserServiceImpl
调用类名:com.xiamu.springboot.service.impl.UserServiceImpl$$FastClassBySpringCGLIB$$8ae4e401 调用方法名:invoke
复制代码
至此已经可以确定是由于SpringAop导致的问题,在list方法中使用this.insertUser()调用是直接使用了userServiceImpl对象,并不是增强后的代理对象。
- SpringAop的分析:
config.isOptimize()
与config.isProxyTargetClass()
都是配置项(网上都说默认是false false,我这个demo使用的是springboot2.0,Debug时发现生成userServiceImpl代理对象时,第二个配置为true),hasNoUserSuppliedProxyInterfaces()
判断代理的对象是否实现了接口。没有实现接口默认使用Cglibpublic AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
// 判断是使用JDK动态代理还是Cglib
if (!config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {
return new JdkDynamicAopProxy(config);
} else {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
} else {
return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
}
}
}
private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
Class<?>[] ifcs = config.getProxiedInterfaces();
return ifcs.length == 0 || ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0]);
}复制代码
调用通过Cglib代理生成的对象中的方法会被CglibAopProxy
中的intercept
方法拦截。在这个方法中this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
是用来获取拦截链的,如果拦截链是空的则直接调用原对象的方法(所以在这个Demo中service中方法调用对象是代理对象,打印this发现是原对象)。像是我们在service中加上的@Transactional
就会在这个拦截链中,后续在invokeWithinTransaction()
方法中会为我们初始化平台事务管理器等相关配置、开启事务、提交或回滚事务等操作(代码就不分析了看不太懂@-@)// 拦截所有目标类方法的调用
@Nullable
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
Object target = null;
TargetSource targetSource = this.advised.getTargetSource();
Object var16;
try {
if (this.advised.exposeProxy) {
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
target = targetSource.getTarget();
Class<?> targetClass = target != null ? target.getClass() : null;
// 获取的拦截器,返回一个拦截链
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = methodProxy.invoke(target, argsToUse);
} else {
retVal = (new CglibAopProxy.CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy)).proceed();
}
retVal = CglibAopProxy.processReturnType(proxy, target, method, retVal);
var16 = retVal;
} finally {
if (target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}
if (setProxyContext) {
AopContext.setCurrentProxy(oldProxy);
}
}
return var16;
}复制代码
摸着石头过河,如果有讲的有问题的地方欢迎大家指出。