资讯专栏INFORMATION COLUMN

【Spring】一次线上@Transational事务注解未生效的原因探究

姘存按 / 2484人阅读

摘要:由于的限制,无法替换被代理类已经被载入的字节码,只能生成并载入一个新的子类作为代理类,被代理类的字节码依然存在于中。区别于前两者,是一种静态代理的实现,即在编译时或者载入类时直接修改被代理类文件的字节码,而非运行时实时生成代理。

现象描述

上周同事发现其基于mySql实现的分布式锁的线上代码存在问题,代码简化如下:

@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管理起来。

发现的两个问题

在一个实例方法中调用被@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()时,由于子类没有覆写父类同名方法,因此实际上执行了父类XServicedoOutside()方法,所以在执行其this.doInside()时实际上调用的是父类未增强过的doInside(),因此事务管理器失效了。

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

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

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

“自调用”的解决方法 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()因此也能正确的指向增强后的方法。

一种错误的解决办法:改造为Java接口的形式
@Controller
class XService implements IXService {
    @Autowired
    private YService yService;
    @Override
    public void doOutside(){
        this.doInside();
    }
    @Transactional
    private void doInside(){
        //do sql statement
    }
}
@Controller
class Test {
    @Autowired
    private IXService iXService;
    public test(){
        iXService.doOutside();
    }
}

原因是之前错误地理解事务未生效的原理:如果没有在xml中要设置只用CGLIB,@Transactional只能使用JDK动态代理,所以如果没有用Java接口方式进行代理就不会生效。

实际上,这还是避免不了自调用的问题,因为这是动态代理的普遍问题,无论是JDK动态代理还是CGLIB动态代理。

总结

使用Spring AOP的时候一定要小心,如果是使用注解形式声明AOP,要保证在被代理类的外部调用被增强的方法。

Reference

Spring AOP 实现原理与 CGLIB 应用

关于spring的aop拦截的问题 protected方法代理问题

透彻的掌握 Spring 中@transactional 的使用

Spring @Transactional原理及使用

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/67694.html

相关文章

  • 一次线上问题排查解决过程

    摘要:排查异常日志,发现没有该问题存在。测试功能正常,没有重现线上问题。解决问题原因定位好了,剩下的就是如何解决了。两个方案修改线上配置该上实施难度系数高,因为公司使用的统一发布部署平台,开发人员无服务器操作权限。 问题 XX系统中,一个用户需要维护的项目数过多,填写的任务数超多,产生了一次工时保存中,只有前面一部分的xx数据持久化到数据库,后面的数据没有保存。 图1 showImg(htt...

    宋华 评论0 收藏0
  • 一次线上问题排查解决过程

    摘要:排查异常日志,发现没有该问题存在。测试功能正常,没有重现线上问题。解决问题原因定位好了,剩下的就是如何解决了。两个方案修改线上配置该上实施难度系数高,因为公司使用的统一发布部署平台,开发人员无服务器操作权限。 问题 XX系统中,一个用户需要维护的项目数过多,填写的任务数超多,产生了一次工时保存中,只有前面一部分的xx数据持久化到数据库,后面的数据没有保存。 图1 showImg(htt...

    airborne007 评论0 收藏0
  • 一次线上问题排查解决过程

    摘要:排查异常日志,发现没有该问题存在。测试功能正常,没有重现线上问题。解决问题原因定位好了,剩下的就是如何解决了。两个方案修改线上配置该上实施难度系数高,因为公司使用的统一发布部署平台,开发人员无服务器操作权限。 问题 XX系统中,一个用户需要维护的项目数过多,填写的任务数超多,产生了一次工时保存中,只有前面一部分的xx数据持久化到数据库,后面的数据没有保存。 图1 showImg(htt...

    HollisChuang 评论0 收藏0
  • Java进阶之路

    摘要:探索专为而设计的将探讨进行了何种改进,以及这些改进背后的原因。关于最友好的文章进阶前言之前就写过一篇关于最友好的文章反响很不错,由于那篇文章的定位就是简单友好,因此尽可能的摒弃复杂的概念,只抓住关键的东西来讲,以保证大家都能看懂。 周月切换日历 一个可以进行周月切换的日历,左右滑动的切换月份,上下滑动可以进行周,月不同的视图切换,可以进行事件的标记,以及节假日的显示,功能丰富 Andr...

    sushi 评论0 收藏0
  • 面试分享:最全Spring事务面试考点整理

    摘要:和事务的关系关系型数据库某些消息队列等产品或中间件称为事务性资源,因为它们本身支持事务,也能够处理事务。事务的传播特性,,,,,,强制要求要有一个物理事务。外围事务不会被内部事务的回滚状态影响。不支持当前事务。 Spring和事务的关系 关系型数据库、某些消息队列等产品或中间件称为事务性资源,因为它们本身支持事务,也能够处理事务。 Spring很显然不是事务性资源,但是它可...

    graf 评论0 收藏0

发表评论

0条评论

姘存按

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<