zoukankan      html  css  js  c++  java
  • Spring嵌套事务失效问题

    现象描述
    代码简化如下:

    @Controller
    class XService {
        @Autowired
        private YService yService;public void doOutside(){
            this.doInside(); //或者直接doInside();效果是一样的
        }
        @Transactional
        private void doInside(){
            //do sql statement
        }
    }
    @Controller
    class Test {
        @Autowired
        private XService xService;
        public void test(){
            xService.doOutside();
        }
    }
    

    实际执行test()后发现doInside()的Sql执行过程没有被Spring Transaction Manager管理起来。

    下面再看另一种情况:

    /**
        * 调用child , 由于child 抛出异常,查看事物回滚
        * @return
        */
       @Transactional(propagation = Propagation.SUPPORTS ,rollbackFor = Exception.class)
       public Integer parent() {
           try {
               child();
           }catch (Exception e){
     
           }
           Course course = new Course();
           course.setName("childCourse");
           course.setCreateTime(new Date());
           testDao .insert(course);
           return Integer.MAX_VALUE;
       }
       /**
        * 被parent 调用,该事物传播机制为新启一个事物,关于事物传播,请另查询资料
        *
        *
        * 插入一条course 记录
        * @return
        */
       @Transactional(propagation = Propagation.REQUIRES_NEW ,rollbackFor = Exception.class)
       public Integer child() {
           Course course = new Course();
           course.setName("childCourse");
           course.setCreateTime(new Date());
           testDao .insert(course);
           // 抛出异常
           int i = 10 / 0 ;
           return Integer.MAX_VALUE;
       }
    

    在child 方法中我声明事物传播为REQUIRES_NEW
    ,因此,child 在执行的时候应该挂起parent 方法的事物,等执行完毕child 方法的事物之后,唤醒parent 的事物,这种情况的预期结果是parent 插入成功,child 插入失败。但是 结果,确实 呵呵,全部成功。
    发现的两个问题
    在一个实例方法中调用被@Transactional注解标记的另一个方法,且两个方法都属于同一个类时,事务不会生效。
    调用被@Transactional注解标记的非public方法,事务不会生效。
    首先复习下相关知识:Spring AOP、JDK动态代理、CGLIB、AspectJ、@Aspect
    @Transactional的实现原理是在业务方法外边通过Spring AOP包上一层事务管理器的代码(即插入切面),这是Java设计模式中常见的通过代理增强被代理类的做法。

    Spring AOP的底层有2种实现:JDK动态代理、CGLIB。前者的原理是JDK反射,并且只支持Java接口的代理;后者的原理是继承(extend)与覆写(override),因此能支持普通的Java类的代理。两种方式都是动态代理,即运行时实时生成代理。

    由于JVM的限制,CGLIB无法替换被代理类已经被载入的字节码,只能生成并载入一个新的子类作为代理类,被代理类的字节码依然存在于JVM中。

    区别于前两者,AspectJ是一种静态代理的实现,即在编译时或者载入类时直接修改被代理类文件的字节码,而非运行时实时生成代理。因此这种方式需要额外的编译器或者JVM Agent支持,通过一些配置Spring和AspectJ也可以配合使用。

    @Aspect一开始是AspectJ推出的Java注解形式,后来Spring AOP也支持使用这种形式表示切面,但实际上底层实现和AspectJ毫无关系,毕竟Spring AOP是动态代理,和静态代理是不兼容的。

    进一步分析
    既然事务管理器没有生效,那么首先需要确定一个问题:this到底是指向哪个对象,是未增强的XService还是增强后的XService?并且而且有没有可能已经调用增强后的实例和方法,但由于其他原因而导致事务管理器没有生效?

    回忆下Java基础,this表示的是类的当前实例,那么关键就是确定类的实例是未被增强的XService(下面称其为XService),还是被CGLIB增强过的XService(下面称其为XService$$Cglib)。

    在Test中,XService类的实例变量是一个由Spring框架管理的Bean,当执行test()时,根据@Autowired注解进行相应的注入,因此XService的实例实际为XService$$Cglib而不XService。被增强过的类的代码可以简化如下:

    class XService$$Cglib extend XService {
        @Override
        public doInside(){
            //开始事务的增强代码
            super.doInside();
            //结束事务的增强代码
        }
    }
    

    当执行XService$$Cglib.doOutside()时,由于子类没有覆写父类同名方法,因此实际上执行了父类XService的doOutside()方法,所以在执行其this.doInside()时实际上调用的是父类未增强过的doInside(),因此事务管理器失效了。

    这个问题在Spring AOP中广泛存在,即自调用,本质上是动态代理无法解决的盲区,只有AspectJ这类静态代理才能解决。

    第二个问题则是Spring AOP不支持非public方法增强,与自调用类似,也是动态代理无法解决的盲区。

    虽然CGLIB通过继承的方式是可以支持public、protected、package级别的方法增强的,但是由于JDK动态代理必须通过Java接口,只能支持public级别的方法,因此Spring AOP不得不取消非public方法的支持。

    下面对接口进行动态代理进行分析
    接口:

    package proxy2;
     
    public interface TestService  {
        Integer test1();
        Integer test2();
        Integer abcTest();
    }
    

    实现类:

    package proxy2;
     
    public class TestServiceImpl implements TestService {
     
    //    public Integer test1() {
    //        System.out.println("test1 被调用");
    //        return Integer.MAX_VALUE;
    //    }
    //
    //    public Integer test2() {
    //        System.out.println("test2 被调用");
    //        return Integer.MAX_VALUE;
    //    }
    //
    //    public Integer abcTest() {
    //        System.out.println("abcTest 被调用");
    //        return null;
    //    }
     
        public Integer test1() {
            System.out.println("test1 被调用");
            test2();
            System.out.println("-------------------------------------------------------");
            return Integer.MAX_VALUE;
        }
     
        public Integer test2() {
            System.out.println("test2 被调用");
            System.out.println("-------------------------------------------------------");
            return Integer.MAX_VALUE;
        }
     
        public Integer abcTest() {
            System.out.println("abcTest 被调用");
            System.out.println("-------------------------------------------------------");
            return null;
        }
    }
    

    代理:

    package proxy2;
     
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
     
    public class MyInvocationHandler implements InvocationHandler {
        private Object target ;
     
        public MyInvocationHandler(Object target){
            this.target = target ;
        }
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if(method.getName().startsWith("test")){
                System.out.println("我被代理了");
            }
            Object invoke = method.invoke(target, args);
            return invoke;
        }
    }
    

    测试:

    package proxy2;
     
    import java.lang.reflect.Proxy;
     
    public class TestProxy {
     
        public static void main(String[] args) {
            MyInvocationHandler invocationHandler = new MyInvocationHandler(new TestServiceImpl());
            TestService testService =(TestService) Proxy.newProxyInstance(
                    TestService.class.getClassLoader(),
                    new TestServiceImpl().getClass().getInterfaces(),
                    invocationHandler);
     
            testService.test1();
            testService.test2();
            testService.abcTest();
        }
    }
    

    结果:

    我被代理了
    test1 被调用
    test2 被调用


    我被代理了
    test2 被调用

    abcTest 被调用

    原因
    Spring AOP使用JDK动态代理和CGLib,当方法被代理时,其实通过动态代理生成了代理对象,然后代理对象执行invoke方法,在调用被代理对象的方法时,执行其他操作。问题就在于被代理对象的方法中调用被代理对象的其他方法时,使用的是被代理对象本身,而非代理对象。这就导致了一个方法时代理对象调用的,一个是被代理对象调用的。他们的调用始终不出于同一个对象。

    “自调用”的解决方法

    1. 最好在被代理类的外部调用其方法
    2. 自注入(Self Injection, from Spring 4.3)
    @Controller
    class XService {
        @Autowired
        private YService yService;
        @Autowired
        private XService xService;
        public void doOutside(){
            xService.doInside();//从this换成了xService
        }
        @Transactional
        private void doInside(){
            //do sql statement
        }
    }
    @Controller
    class Test {
        @Autowired
        private XService xService;
        public void test(){
            xService.doOutside();
        }
    }
    

    由于xService变量是被Spring注入的,因此实际上指向XService$$Cglib对象,xService.doInside()因此也能正确的指向增强后的方法。

    3.通过ThreadLocal暴露Aop代理对象
    1、开启暴露Aop代理到ThreadLocal支持(如下配置方式从spring3开始支持)
    <aop:aspectj-autoproxy expose-proxy="true"/><!—注解风格支持-->
    <aop:config expose-proxy="true"><!—xml风格支持-->

    2、修改我们的业务实现类

    @Transactional(propagation = Propagation.SUPPORTS,rollbackFor = Exception.class)
        public Integer parent() {
            try {
                ((TestService) AopContext.currentProxy()). child();
            }catch (Exception e){
     
            }
            Course course = new Course();
            course.setName("parent");
            course.setCreateTime(new Date());
            testDao .insert(course);
            return Integer.MAX_VALUE;
        }
    

    配置

    <tx:annotation-driven transaction-manager="transactionManager"/>
    <!-- (事务管理)transaction manager, use JtaTransactionManager for global tx -->
    <bean id="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <property name="dataSource" ref="dataSourceForSqlServer" />
    </bean>
    

    注意事项
    1.在需要事务管理的地方加@Transactional 注解。@Transactional 注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上 。
    2.@Transactional 注解只能应用到 public 可见度的方法上 。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错, 但是这个被注解的方法将不会展示已配置的事务设置。
    3.注意仅仅 @Transactional 注解的出现不足于开启事务行为,它仅仅 是一种元数据。必须在配置文件中使用配置元素,才真正开启了事务.
    4.spring事物是基于类和接口的所以只能在类里面调用另一个类里面的事物,同一个类里面调用自己类的事物方法是无效的。spring事物也不要频繁使用,在事物处理的同时操作的第一张表会被限制查看的(即被临时锁住)。数据量大的时候会有一定影响。

    摘自: https://blog.csdn.net/u014082714/article/details/80967103

  • 相关阅读:
    linux安装tmux分屏插件
    美化linux客户端zsh和oh-my-zsh
    使用Feign通过服务名调用服务,找不到服务
    Docker Compose 配置文件详解
    Dockerfile构建jar镜像
    Mysql数据库主从同步
    【Java】Java划水练习
    【2-SAT】Codeforces Round #403 (Div. 2, based on Technocup 2017 Finals) D. Innokenty and a Football League
    【贪心】【DFS】Codeforces Round #403 (Div. 2, based on Technocup 2017 Finals) C. Andryusha and Colored Balloons
    【三分】Codeforces Round #403 (Div. 2, based on Technocup 2017 Finals) B. The Meeting Place Cannot Be Changed
  • 原文地址:https://www.cnblogs.com/kevliudm/p/11121601.html
Copyright © 2011-2022 走看看