原創(chuàng)文章,轉(zhuǎn)載請(qǐng)標(biāo)注出處:《Spring基礎(chǔ)系列-Spring事務(wù)不生效的問(wèn)題與循環(huán)依賴問(wèn)題》
一、提出問(wèn)題
不知道你是否遇到過(guò)這樣的情況,在ssm框架中開(kāi)發(fā)web應(yīng)用,或者使用springboot開(kāi)發(fā)應(yīng)用,當(dāng)我們調(diào)用一個(gè)帶有@Transactional注解的方法執(zhí)行某項(xiàng)事務(wù)操作的時(shí)候,有時(shí)候會(huì)發(fā)現(xiàn)事務(wù)是不生效的。
你是否考慮過(guò)這是為什么,又該如何來(lái)修復(fù)事務(wù)呢?
二、分析問(wèn)題
要想弄明白事務(wù)不生效的原因,我們首先要弄明白Spring中事務(wù)的實(shí)現(xiàn)原理,而Spring中的聲明式事務(wù)是使用AOP來(lái)實(shí)現(xiàn)的。
Spring中AOP又是依靠什么實(shí)現(xiàn)的呢?動(dòng)態(tài)代理,在Spring中使用的兩種動(dòng)態(tài)代理,一種是java原生提供的JDK動(dòng)態(tài)代理,另一種是第三方提供的CGLIB動(dòng)態(tài)代理,前者基于接口實(shí)現(xiàn),后者基于類實(shí)現(xiàn),明顯后者的適用范圍更加廣泛,但是原生的JDK動(dòng)態(tài)代理卻是速度要快很多,兩者各有特色。

動(dòng)態(tài)代理的目的就是在應(yīng)用運(yùn)行時(shí)實(shí)時(shí)生成代理類,這樣我們就能在已有實(shí)現(xiàn)的基礎(chǔ)上對(duì)其進(jìn)行增強(qiáng),這其實(shí)也就是AOP的目的所在,增強(qiáng)類的功能。
動(dòng)態(tài)代理生成的代理類擁有原生類的所有公有方法,針對(duì)指定方法的調(diào)用會(huì)轉(zhuǎn)移到代理類的同名方法之上,而在這個(gè)方法之內(nèi)會(huì)在調(diào)用原生類的同名方法之外進(jìn)行一些其他的操作,比如日志記錄,比如安全檢查,比如事務(wù)操作等。
當(dāng)我們?cè)贑ontroller層直接調(diào)用service層的一個(gè)帶有事務(wù)注解的方法時(shí),就會(huì)執(zhí)行以上步驟:生成代理類,調(diào)用代理類的同名方法,由代理類實(shí)現(xiàn)事務(wù)功能,再調(diào)用原生類的方法進(jìn)行邏輯執(zhí)行。
上面這種情況是沒(méi)有問(wèn)題的,有問(wèn)題的是我們?cè)趕ervice層內(nèi)部的方法調(diào)用本類中的帶有事務(wù)注解的方法時(shí),該事務(wù)注解將失效,我們的調(diào)用方式無(wú)非就是直接調(diào)用或者用this調(diào)用,這兩種情況效果其實(shí)是一樣的,都是用當(dāng)前實(shí)例調(diào)用。
結(jié)合之前的AOP和動(dòng)態(tài)代理的介紹,我們很容易就能理解這里事務(wù)失效的原因:那就是我們調(diào)用目標(biāo)事務(wù)方法的時(shí)候直接調(diào)用的原生的方法,而沒(méi)有調(diào)用代理類中的代理方法,也就是說(shuō),我們沒(méi)有調(diào)用進(jìn)行了事務(wù)增強(qiáng)的方法,如此一來(lái)事務(wù)當(dāng)然會(huì)失效了。
這么來(lái)說(shuō),我們需要調(diào)用代理類中增強(qiáng)之后的代理方法,才能使事務(wù)生效。
三、解決問(wèn)題
那么我們要如何來(lái)修復(fù)呢?其實(shí)很簡(jiǎn)單,只要我們不使用this調(diào)用即可。this代表的是當(dāng)前實(shí)例,在spring中一般就是單例實(shí)例,自己調(diào)用自己的方法,事務(wù)注解等于擺設(shè)。如果我們更改調(diào)用方式,在當(dāng)前類中注入自身單例實(shí)例,使用注入的實(shí)例來(lái)調(diào)用該方法,即可使事務(wù)生效。
為什么呢?一般我們的SSM架構(gòu)中的Service層都是有接口和實(shí)現(xiàn)類的,既然存在接口,那么這里使用的必然是JDK動(dòng)態(tài)代理來(lái)生成代理類。當(dāng)我們將當(dāng)前類的單例實(shí)例注入到自身之后,使用這個(gè)注入的實(shí)例來(lái)調(diào)用接口中的方法時(shí),如果存在@Transactional之類的AOP增強(qiáng)注解存在,那么就是生成代理類來(lái)實(shí)現(xiàn)功能增強(qiáng)。(在Springboot中開(kāi)發(fā)的時(shí)候我們習(xí)慣去掉接口開(kāi)發(fā),那么代理類就是使用CGLIB動(dòng)態(tài)代理生成的)。
這樣也就要求我們的事務(wù)方法需要先在接口中聲明,然后在實(shí)現(xiàn)類中實(shí)現(xiàn)邏輯,并添加事務(wù)注解。
這種方式適用于解決在Service中調(diào)用Service中的事務(wù)方法時(shí)事務(wù)失效的問(wèn)題。這么想想之前從Controller調(diào)用Service的時(shí)候也是通過(guò)注入的Service單例實(shí)例來(lái)調(diào)用的,這也側(cè)面證明我們提供的方法時(shí)有效的。
還有幾種解決方案:
- 一種就是Spring基礎(chǔ)系列-AOP源碼分析中的源碼6里面所說(shuō)的通過(guò)暴露AOP代理的方式實(shí)現(xiàn)。
- 一種是將事務(wù)注解添加到類上。
- 再一種就是就是將被調(diào)用的事務(wù)方法,放到另一個(gè)類中再進(jìn)行調(diào)用。
- 這里再添加一種方法:使當(dāng)前類實(shí)現(xiàn)BeanFactoryAware接口,并實(shí)現(xiàn)setBeanFactory方法,添加BeanFactory字段,然后通過(guò)beanFactory的getBean方法獲取當(dāng)前類的Bean實(shí)例來(lái)調(diào)用目標(biāo)事務(wù)方法,即可實(shí)現(xiàn)嵌套之類的事務(wù)調(diào)用。
四、問(wèn)題引申
4.1 引申問(wèn)題:循環(huán)依賴
至于由此引發(fā)的另一個(gè)問(wèn)題:當(dāng)我們?cè)诋?dāng)前類中注入當(dāng)前類的實(shí)例后,在創(chuàng)建這個(gè)類的實(shí)例的時(shí)候是需要注入這個(gè)類的實(shí)例的,但是這時(shí)候這個(gè)類有沒(méi)有創(chuàng)建完成,這該怎么辦呢???
這就是Spring中著名的循環(huán)依賴問(wèn)題。
更明顯的樣例是在A中依賴B,B中又依賴A的情況,依賴相互彼此,那么會(huì)不會(huì)導(dǎo)致兩個(gè)實(shí)例都創(chuàng)建失敗呢?
4.2 循環(huán)依賴的解決方案
有必要簡(jiǎn)單說(shuō)下Spring中針對(duì)這個(gè)問(wèn)題的解決方案。為什么是簡(jiǎn)單介紹呢,因?yàn)槲乙仓皇呛?jiǎn)單理解,但是這種簡(jiǎn)單理解更加適用于不明白的朋友,不至于一來(lái)就懵逼。
我們都知道在Spring中Bean有多種生命周期范圍,主要就是單例和原型(當(dāng)然還有request、Session等范圍),單例表示在整個(gè)應(yīng)用上下文中只會(huì)存在一個(gè)Bean實(shí)例,而原型正好相反,可以存在多個(gè)Bean實(shí)例,每次調(diào)用getBean的時(shí)候都會(huì)新建一個(gè)新的bean實(shí)例。
我們要強(qiáng)調(diào),在Spring中原型范圍的Bean實(shí)例如果發(fā)生循環(huán)依賴,只有一種下場(chǎng):拋異常。
而針對(duì)單例bean,Spring內(nèi)部提供了一種有效的提前暴露的機(jī)制解決了循環(huán)依賴的問(wèn)題。當(dāng)然這里僅僅解決的是使用setter方式實(shí)現(xiàn)依賴注入的情況,如果是使用構(gòu)造器依賴注入的情況還是那種下場(chǎng):拋異常。
拋異常代表,Spring無(wú)能力解決此問(wèn)題,程序出錯(cuò)。
為什么呢?難道Spring不想解決嗎?肯定不是,而是無(wú)能為力罷了。
我們先簡(jiǎn)單了解下setter方式實(shí)現(xiàn)依賴注入的單例Bean的循環(huán)依賴的解決方法:
先介紹下Spring中的那幾個(gè)緩存池:
- singletonObjects:?jiǎn)卫彺娉兀糜诒4鎰?chuàng)建完成的單例Bean,是Map,凡是創(chuàng)建完畢的Bean實(shí)例全部保存在該緩存池中,不存在循環(huán)依賴的Bean會(huì)直接在創(chuàng)建完之后保存到該緩存中,而存在循環(huán)依賴的bean則會(huì)在其創(chuàng)建完成后由earlySingletonObjects轉(zhuǎn)移到此緩存中。
- singletonFactories:?jiǎn)卫S緩存池,用于保存提前暴露的ObjectFactory,是Map。
- earlySingletonObjects:早期單例緩存池,用于保存尚未創(chuàng)建完成的用于早期暴露的單例Bean,是Map,它與singletonObjects是互斥的,就是不可能同時(shí)保存于兩者之中,只能擇一而存,保存在該緩存池中的是尚未完成創(chuàng)建,而被注入到其他Bean中的Bean實(shí)例,可以說(shuō)該緩存就是一個(gè)中間緩存(或者叫過(guò)程緩存),只在當(dāng)將該BeanName對(duì)應(yīng)的原生Bean(處于創(chuàng)建中池)注入到另一個(gè)bean實(shí)例中后,將其添加到該緩存中,這個(gè)緩存中保存的永遠(yuǎn)是半成品的bean實(shí)例,當(dāng)Bean實(shí)例最終完成創(chuàng)建后會(huì)從此緩存中移除,轉(zhuǎn)移到singletonObjects緩存中保存。
- registeredSingletons:已注冊(cè)的單例緩存池,用于保存已完成創(chuàng)建的Bean實(shí)例的beanName,是Set(此緩存未涉及)。
- singletonsCurrentlyInCreation:創(chuàng)建中池,保存處于創(chuàng)建中的單例bean的BeanName,是Set,在這個(gè)bean實(shí)例開(kāi)始創(chuàng)建時(shí)添加到池中,而來(lái)Bean實(shí)例創(chuàng)建完成之后從池中移除。
當(dāng)存在循環(huán)依賴的情況時(shí),比如之前的情況:A依賴B,B又依賴A的情況,這種情況下,首先要?jiǎng)?chuàng)建A實(shí)例,將其beanName添加到singletonsCurrentlyInCreation池,然后調(diào)用A的構(gòu)造器創(chuàng)建A的原生實(shí)例,并將其ObjectFactory添加到singletonFactories緩存中,然后處理依賴注入(B實(shí)例),發(fā)現(xiàn)B實(shí)例不存在且也不在singletonsCurrentlyInCreation池中,表示Bean實(shí)例尚未進(jìn)行創(chuàng)建,那么下一步開(kāi)始創(chuàng)建B實(shí)例,將其beanName添加到singletonsCurrentlyInCreation池,然后調(diào)用B的構(gòu)造器創(chuàng)建A的原生實(shí)例,并將其ObjectFactory添加到singletonFactories緩存中,再然后處理依賴注入(A實(shí)例),發(fā)現(xiàn)A實(shí)例尚未創(chuàng)建完成,但在singletonsCurrentlyInCreation池中發(fā)現(xiàn)了A實(shí)例的beanName,說(shuō)明A實(shí)例正處于創(chuàng)建中,這時(shí)表示出現(xiàn)循環(huán)依賴,Spring會(huì)將singletonFactories緩存中獲取對(duì)應(yīng)A的beanName的ObjectFactory中g(shù)etObject方法返回的Bean實(shí)例注入到B中,來(lái)完成B實(shí)例的創(chuàng)建步驟,同時(shí)也會(huì)將A的Bean實(shí)例添加到earlySingletonObjects緩存中,表示A實(shí)例是一個(gè)提前暴露的Bean實(shí)例,B實(shí)例創(chuàng)建完畢之后需要將B的原生實(shí)例從singletonFactories緩存中移除,并將完整實(shí)例添加到SingletonObjects緩存中(當(dāng)然earlySingletonObjects中也不能存在),并且將其beanName從singletonsCurrentlyInCreation池中移除(表示B實(shí)例完全創(chuàng)建完畢)。然后將B實(shí)例注入到A實(shí)例中來(lái)完成A實(shí)例的創(chuàng)建,最后同樣將A的原生實(shí)例從earlySingletonObjects中移除,完整實(shí)例添加到SingletonObjects中,并將A的beanName從創(chuàng)建中池中移除。到此完成A和B兩個(gè)單例實(shí)例的創(chuàng)建。
了解了上面所述的解決方案之后,我們可以明白針對(duì)構(gòu)造器實(shí)現(xiàn)依賴注入的Bean發(fā)生循環(huán)依賴的情況下為什么無(wú)法解決。那就是因?yàn)?,之前提前暴露的前提是?chuàng)建好原生的Bean實(shí)例,原生的Bean實(shí)例就是依靠構(gòu)造器創(chuàng)建的,如果在構(gòu)造器創(chuàng)建Bean的時(shí)候就需要注入依賴,而依賴又正處于創(chuàng)建中的話,由于無(wú)法暴露ObjectFactory,而無(wú)法解決循環(huán)依賴問(wèn)題。
另外原型bean的情況,Spring根本就不會(huì)對(duì)原型的Bean添加緩存,因?yàn)樘砑泳彺娴哪康氖菫榱吮WC單例Bean的唯一性,但是對(duì)于原型,就不能緩存了,如果從緩存獲取的Bean實(shí)例,那還是原型模式嗎?不存在緩存當(dāng)然也就無(wú)法實(shí)現(xiàn)上面描述的那一系列操作,也就無(wú)法解決循環(huán)依賴的問(wèn)題了。
五、總結(jié)
Spring中的事務(wù)問(wèn)題歸結(jié)為注入問(wèn)題,循環(huán)依賴問(wèn)題也是注入問(wèn)題,有關(guān)注入的問(wèn)題以后再討論。
Spring之中所有的增強(qiáng)都是依靠AOP實(shí)現(xiàn)的,而AOP又是依靠動(dòng)態(tài)代理實(shí)現(xiàn)的,JDK的動(dòng)態(tài)代理依靠反射技術(shù)實(shí)現(xiàn),而CGLIB動(dòng)態(tài)代理依靠字節(jié)碼技術(shù)實(shí)現(xiàn)。