Spring基础系列-Spring事务不生效的问题与循环依赖问题


原创文章,转载请标注出处:《Spring基础系列-Spring事务不生效的问题与循环依赖问题》


一、提出问题

不知道你是否遇到过这样的情况,在ssm框架中开发web应用,或者使用springboot开发应用,当我们调用一个带有@Transactional注解的方法执行某项事务操作的时候,有时候会发现事务是不生效的。

你是否考虑过这是为什么,又该如何来修复事务呢?

二、分析问题

要想弄明白事务不生效的原因,我们首先要弄明白Spring中事务的实现原理,而Spring中的声明式事务是使用AOP来实现的。

Spring中AOP又是依靠什么实现的呢?动态代理,在Spring中使用的两种动态代理,一种是java原生提供的JDK动态代理,另一种是第三方提供的CGLIB动态代理,前者基于接口实现,后者基于类实现,明显后者的适用范围更加广泛,但是原生的JDK动态代理却是速度要快很多,两者各有特色。

动态代理的目的就是在应用运行时实时生成代理类,这样我们就能在已有实现的基础上对其进行增强,这其实也就是AOP的目的所在,增强类的功能。

动态代理生成的代理类拥有原生类的所有公有方法,针对指定方法的调用会转移到代理类的同名方法之上,而在这个方法之内会在调用原生类的同名方法之外进行一些其他的操作,比如日志记录,比如安全检查,比如事务操作等。

当我们在Controller层直接调用service层的一个带有事务注解的方法时,就会执行以上步骤:生成代理类,调用代理类的同名方法,由代理类实现事务功能,再调用原生类的方法进行逻辑执行。

上面这种情况是没有问题的,有问题的是我们在service层内部的方法调用本类中的带有事务注解的方法时,该事务注解将失效,我们的调用方式无非就是直接调用或者用this调用,这两种情况效果其实是一样的,都是用当前实例调用。

结合之前的AOP和动态代理的介绍,我们很容易就能理解这里事务失效的原因:那就是我们调用目标事务方法的时候直接调用的原生的方法,而没有调用代理类中的代理方法,也就是说,我们没有调用进行了事务增强的方法,如此一来事务当然会失效了。

这么来说,我们需要调用代理类中增强之后的代理方法,才能使事务生效。

三、解决问题

那么我们要如何来修复呢?其实很简单,只要我们不使用this调用即可。this代表的是当前实例,在spring中一般就是单例实例,自己调用自己的方法,事务注解等于摆设。如果我们更改调用方式,在当前类中注入自身单例实例,使用注入的实例来调用该方法,即可使事务生效。

为什么呢?一般我们的SSM架构中的Service层都是有接口和实现类的,既然存在接口,那么这里使用的必然是JDK动态代理来生成代理类。当我们将当前类的单例实例注入到自身之后,使用这个注入的实例来调用接口中的方法时,如果存在@Transactional之类的AOP增强注解存在,那么就是生成代理类来实现功能增强。(在Springboot中开发的时候我们习惯去掉接口开发,那么代理类就是使用CGLIB动态代理生成的)。

这样也就要求我们的事务方法需要先在接口中声明,然后在实现类中实现逻辑,并添加事务注解。

这种方式适用于解决在Service中调用Service中的事务方法时事务失效的问题。这么想想之前从Controller调用Service的时候也是通过注入的Service单例实例来调用的,这也侧面证明我们提供的方法时有效的。

还有几种解决方案:

  • 一种就是Spring基础系列-AOP源码分析中的源码6里面所说的通过暴露AOP代理的方式实现。
  • 一种是将事务注解添加到类上。
  • 再一种就是就是将被调用的事务方法,放到另一个类中再进行调用。
  • 这里再添加一种方法:使当前类实现BeanFactoryAware接口,并实现setBeanFactory方法,添加BeanFactory字段,然后通过beanFactory的getBean方法获取当前类的Bean实例来调用目标事务方法,即可实现嵌套之类的事务调用。

四、问题引申

4.1 引申问题:循环依赖

至于由此引发的另一个问题:当我们在当前类中注入当前类的实例后,在创建这个类的实例的时候是需要注入这个类的实例的,但是这时候这个类有没有创建完成,这该怎么办呢???

这就是Spring中著名的循环依赖问题。

更明显的样例是在A中依赖B,B中又依赖A的情况,依赖相互彼此,那么会不会导致两个实例都创建失败呢?

4.2 循环依赖的解决方案

有必要简单说下Spring中针对这个问题的解决方案。为什么是简单介绍呢,因为我也只是简单理解,但是这种简单理解更加适用于不明白的朋友,不至于一来就懵逼。

我们都知道在Spring中Bean有多种生命周期范围,主要就是单例和原型(当然还有request、Session等范围),单例表示在整个应用上下文中只会存在一个Bean实例,而原型正好相反,可以存在多个Bean实例,每次调用getBean的时候都会新建一个新的bean实例。

我们要强调,在Spring中原型范围的Bean实例如果发生循环依赖,只有一种下场:抛异常。

而针对单例bean,Spring内部提供了一种有效的提前暴露的机制解决了循环依赖的问题。当然这里仅仅解决的是使用setter方式实现依赖注入的情况,如果是使用构造器依赖注入的情况还是那种下场:抛异常。

抛异常代表,Spring无能力解决此问题,程序出错。

为什么呢?难道Spring不想解决吗?肯定不是,而是无能为力罢了。

我们先简单了解下setter方式实现依赖注入的单例Bean的循环依赖的解决方法:

先介绍下Spring中的那几个缓存池:

  • singletonObjects:单例缓存池,用于保存创建完成的单例Bean,是Map,凡是创建完毕的Bean实例全部保存在该缓存池中,不存在循环依赖的Bean会直接在创建完之后保存到该缓存中,而存在循环依赖的bean则会在其创建完成后由earlySingletonObjects转移到此缓存中。
  • singletonFactories:单例工厂缓存池,用于保存提前暴露的ObjectFactory,是Map。
  • earlySingletonObjects:早期单例缓存池,用于保存尚未创建完成的用于早期暴露的单例Bean,是Map,它与singletonObjects是互斥的,就是不可能同时保存于两者之中,只能择一而存,保存在该缓存池中的是尚未完成创建,而被注入到其他Bean中的Bean实例,可以说该缓存就是一个中间缓存(或者叫过程缓存),只在当将该BeanName对应的原生Bean(处于创建中池)注入到另一个bean实例中后,将其添加到该缓存中,这个缓存中保存的永远是半成品的bean实例,当Bean实例最终完成创建后会从此缓存中移除,转移到singletonObjects缓存中保存。
  • registeredSingletons:已注册的单例缓存池,用于保存已完成创建的Bean实例的beanName,是Set(此缓存未涉及)。
  • singletonsCurrentlyInCreation:创建中池,保存处于创建中的单例bean的BeanName,是Set,在这个bean实例开始创建时添加到池中,而来Bean实例创建完成之后从池中移除。

当存在循环依赖的情况时,比如之前的情况:A依赖B,B又依赖A的情况,这种情况下,首先要创建A实例,将其beanName添加到singletonsCurrentlyInCreation池,然后调用A的构造器创建A的原生实例,并将其ObjectFactory添加到singletonFactories缓存中,然后处理依赖注入(B实例),发现B实例不存在且也不在singletonsCurrentlyInCreation池中,表示Bean实例尚未进行创建,那么下一步开始创建B实例,将其beanName添加到singletonsCurrentlyInCreation池,然后调用B的构造器创建A的原生实例,并将其ObjectFactory添加到singletonFactories缓存中,再然后处理依赖注入(A实例),发现A实例尚未创建完成,但在singletonsCurrentlyInCreation池中发现了A实例的beanName,说明A实例正处于创建中,这时表示出现循环依赖,Spring会将singletonFactories缓存中获取对应A的beanName的ObjectFactory中getObject方法返回的Bean实例注入到B中,来完成B实例的创建步骤,同时也会将A的Bean实例添加到earlySingletonObjects缓存中,表示A实例是一个提前暴露的Bean实例,B实例创建完毕之后需要将B的原生实例从singletonFactories缓存中移除,并将完整实例添加到SingletonObjects缓存中(当然earlySingletonObjects中也不能存在),并且将其beanName从singletonsCurrentlyInCreation池中移除(表示B实例完全创建完毕)。然后将B实例注入到A实例中来完成A实例的创建,最后同样将A的原生实例从earlySingletonObjects中移除,完整实例添加到SingletonObjects中,并将A的beanName从创建中池中移除。到此完成A和B两个单例实例的创建。

了解了上面所述的解决方案之后,我们可以明白针对构造器实现依赖注入的Bean发生循环依赖的情况下为什么无法解决。那就是因为,之前提前暴露的前提是创建好原生的Bean实例,原生的Bean实例就是依靠构造器创建的,如果在构造器创建Bean的时候就需要注入依赖,而依赖又正处于创建中的话,由于无法暴露ObjectFactory,而无法解决循环依赖问题。

另外原型bean的情况,Spring根本就不会对原型的Bean添加缓存,因为添加缓存的目的是为了保证单例Bean的唯一性,但是对于原型,就不能缓存了,如果从缓存获取的Bean实例,那还是原型模式吗?不存在缓存当然也就无法实现上面描述的那一系列操作,也就无法解决循环依赖的问题了。

五、总结

Spring中的事务问题归结为注入问题,循环依赖问题也是注入问题,有关注入的问题以后再讨论。

Spring之中所有的增强都是依靠AOP实现的,而AOP又是依靠动态代理实现的,JDK的动态代理依靠反射技术实现,而CGLIB动态代理依靠字节码技术实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342