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

posted @ 2019-07-02 16:43  灬毛毛  阅读(3696)  评论(0编辑  收藏  举报