从事务问题分析SpringAop

博客围绕项目中数据初始化时的事务问题展开。用Postman调用接口,发现listUser方法中调用insertUser方法事务未开启,异常不回滚。经分析,Spring事务由AOP和动态代理实现,最终发现从Spring中获取代理对象调用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,出现了异常但是并没有回滚



  • 原因分析:
  1. spring中的事务是由aop实现的,spring中的aop又是由动态代理实现的,这个时候我们从这两方面去思考这个问题
  2. 将数据删除,在两个方法头部加上如下代码再次运行一次查看打印的信息

@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的分析:
SpringAop使用了动态代理实现,有JDK动态代理(需要实现接口)和Cglib代理(在运行期间生成的代理对象是针对目标类扩展的子类)两种方式。Spring中使用动态代理的规则如下,config.isOptimize()config.isProxyTargetClass()都是配置项(网上都说默认是false false,我这个demo使用的是springboot2.0,Debug时发现生成userServiceImpl代理对象时,第二个配置为true),hasNoUserSuppliedProxyInterfaces()判断代理的对象是否实现了接口。没有实现接口默认使用Cglib

public 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;
}复制代码
摸着石头过河,如果有讲的有问题的地方欢迎大家指出。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值