Spring基礎(chǔ)系列-Spring事務(wù)不生效的問題與循環(huán)依賴問題


原創(chuàng)文章县钥,轉(zhuǎn)載請標(biāo)注出處:《Spring基礎(chǔ)系列-Spring事務(wù)不生效的問題與循環(huán)依賴問題》


一、提出問題

不知道你是否遇到過這樣的情況慈迈,在ssm框架中開發(fā)web應(yīng)用若贮,或者使用springboot開發(fā)應(yīng)用,當(dāng)我們調(diào)用一個帶有@Transactional注解的方法執(zhí)行某項事務(wù)操作的時候痒留,有時候會發(fā)現(xiàn)事務(wù)是不生效的谴麦。

你是否考慮過這是為什么,又該如何來修復(fù)事務(wù)呢伸头?

二细移、分析問題

要想弄明白事務(wù)不生效的原因,我們首先要弄明白Spring中事務(wù)的實現(xiàn)原理熊锭,而Spring中的聲明式事務(wù)是使用AOP來實現(xiàn)的弧轧。

Spring中AOP又是依靠什么實現(xiàn)的呢?動態(tài)代理碗殷,在Spring中使用的兩種動態(tài)代理精绎,一種是java原生提供的JDK動態(tài)代理,另一種是第三方提供的CGLIB動態(tài)代理锌妻,前者基于接口實現(xiàn)代乃,后者基于類實現(xiàn),明顯后者的適用范圍更加廣泛仿粹,但是原生的JDK動態(tài)代理卻是速度要快很多搁吓,兩者各有特色。

動態(tài)代理的目的就是在應(yīng)用運行時實時生成代理類吭历,這樣我們就能在已有實現(xiàn)的基礎(chǔ)上對其進(jìn)行增強堕仔,這其實也就是AOP的目的所在,增強類的功能晌区。

動態(tài)代理生成的代理類擁有原生類的所有公有方法摩骨,針對指定方法的調(diào)用會轉(zhuǎn)移到代理類的同名方法之上,而在這個方法之內(nèi)會在調(diào)用原生類的同名方法之外進(jìn)行一些其他的操作朗若,比如日志記錄恼五,比如安全檢查,比如事務(wù)操作等哭懈。

當(dāng)我們在Controller層直接調(diào)用service層的一個帶有事務(wù)注解的方法時灾馒,就會執(zhí)行以上步驟:生成代理類,調(diào)用代理類的同名方法遣总,由代理類實現(xiàn)事務(wù)功能睬罗,再調(diào)用原生類的方法進(jìn)行邏輯執(zhí)行轨功。

上面這種情況是沒有問題的,有問題的是我們在service層內(nèi)部的方法調(diào)用本類中的帶有事務(wù)注解的方法時傅物,該事務(wù)注解將失效,我們的調(diào)用方式無非就是直接調(diào)用或者用this調(diào)用琉预,這兩種情況效果其實是一樣的董饰,都是用當(dāng)前實例調(diào)用。

結(jié)合之前的AOP和動態(tài)代理的介紹圆米,我們很容易就能理解這里事務(wù)失效的原因:那就是我們調(diào)用目標(biāo)事務(wù)方法的時候直接調(diào)用的原生的方法卒暂,而沒有調(diào)用代理類中的代理方法,也就是說娄帖,我們沒有調(diào)用進(jìn)行了事務(wù)增強的方法也祠,如此一來事務(wù)當(dāng)然會失效了。

這么來說近速,我們需要調(diào)用代理類中增強之后的代理方法诈嘿,才能使事務(wù)生效。

三削葱、解決問題

那么我們要如何來修復(fù)呢奖亚?其實很簡單,只要我們不使用this調(diào)用即可析砸。this代表的是當(dāng)前實例昔字,在spring中一般就是單例實例,自己調(diào)用自己的方法首繁,事務(wù)注解等于擺設(shè)作郭。如果我們更改調(diào)用方式,在當(dāng)前類中注入自身單例實例弦疮,使用注入的實例來調(diào)用該方法夹攒,即可使事務(wù)生效。

為什么呢胁塞?一般我們的SSM架構(gòu)中的Service層都是有接口和實現(xiàn)類的芹助,既然存在接口,那么這里使用的必然是JDK動態(tài)代理來生成代理類闲先。當(dāng)我們將當(dāng)前類的單例實例注入到自身之后状土,使用這個注入的實例來調(diào)用接口中的方法時,如果存在@Transactional之類的AOP增強注解存在伺糠,那么就是生成代理類來實現(xiàn)功能增強蒙谓。(在Springboot中開發(fā)的時候我們習(xí)慣去掉接口開發(fā),那么代理類就是使用CGLIB動態(tài)代理生成的)训桶。

這樣也就要求我們的事務(wù)方法需要先在接口中聲明累驮,然后在實現(xiàn)類中實現(xiàn)邏輯酣倾,并添加事務(wù)注解。

這種方式適用于解決在Service中調(diào)用Service中的事務(wù)方法時事務(wù)失效的問題谤专。這么想想之前從Controller調(diào)用Service的時候也是通過注入的Service單例實例來調(diào)用的躁锡,這也側(cè)面證明我們提供的方法時有效的。

還有幾種解決方案:

  • 一種就是Spring基礎(chǔ)系列-AOP源碼分析中的源碼6里面所說的通過暴露AOP代理的方式實現(xiàn)置侍。
  • 一種是將事務(wù)注解添加到類上映之。
  • 再一種就是就是將被調(diào)用的事務(wù)方法,放到另一個類中再進(jìn)行調(diào)用蜡坊。
  • 這里再添加一種方法:使當(dāng)前類實現(xiàn)BeanFactoryAware接口杠输,并實現(xiàn)setBeanFactory方法,添加BeanFactory字段秕衙,然后通過beanFactory的getBean方法獲取當(dāng)前類的Bean實例來調(diào)用目標(biāo)事務(wù)方法蠢甲,即可實現(xiàn)嵌套之類的事務(wù)調(diào)用。

四据忘、問題引申

4.1 引申問題:循環(huán)依賴

至于由此引發(fā)的另一個問題:當(dāng)我們在當(dāng)前類中注入當(dāng)前類的實例后鹦牛,在創(chuàng)建這個類的實例的時候是需要注入這個類的實例的,但是這時候這個類有沒有創(chuàng)建完成勇吊,這該怎么辦呢能岩??萧福?

這就是Spring中著名的循環(huán)依賴問題拉鹃。

更明顯的樣例是在A中依賴B,B中又依賴A的情況鲫忍,依賴相互彼此膏燕,那么會不會導(dǎo)致兩個實例都創(chuàng)建失敗呢?

4.2 循環(huán)依賴的解決方案

有必要簡單說下Spring中針對這個問題的解決方案悟民。為什么是簡單介紹呢坝辫,因為我也只是簡單理解,但是這種簡單理解更加適用于不明白的朋友射亏,不至于一來就懵逼近忙。

我們都知道在Spring中Bean有多種生命周期范圍,主要就是單例和原型(當(dāng)然還有request智润、Session等范圍)及舍,單例表示在整個應(yīng)用上下文中只會存在一個Bean實例,而原型正好相反窟绷,可以存在多個Bean實例锯玛,每次調(diào)用getBean的時候都會新建一個新的bean實例。

我們要強調(diào),在Spring中原型范圍的Bean實例如果發(fā)生循環(huán)依賴攘残,只有一種下場:拋異常拙友。

而針對單例bean,Spring內(nèi)部提供了一種有效的提前暴露的機制解決了循環(huán)依賴的問題歼郭。當(dāng)然這里僅僅解決的是使用setter方式實現(xiàn)依賴注入的情況遗契,如果是使用構(gòu)造器依賴注入的情況還是那種下場:拋異常。

拋異常代表病曾,Spring無能力解決此問題牍蜂,程序出錯。

為什么呢知态?難道Spring不想解決嗎捷兰?肯定不是立叛,而是無能為力罷了负敏。

我們先簡單了解下setter方式實現(xiàn)依賴注入的單例Bean的循環(huán)依賴的解決方法:

先介紹下Spring中的那幾個緩存池:

  • singletonObjects:單例緩存池,用于保存創(chuàng)建完成的單例Bean秘蛇,是Map其做,凡是創(chuàng)建完畢的Bean實例全部保存在該緩存池中,不存在循環(huán)依賴的Bean會直接在創(chuàng)建完之后保存到該緩存中赁还,而存在循環(huán)依賴的bean則會在其創(chuàng)建完成后由earlySingletonObjects轉(zhuǎn)移到此緩存中妖泄。
  • singletonFactories:單例工廠緩存池,用于保存提前暴露的ObjectFactory艘策,是Map蹈胡。
  • earlySingletonObjects:早期單例緩存池,用于保存尚未創(chuàng)建完成的用于早期暴露的單例Bean朋蔫,是Map罚渐,它與singletonObjects是互斥的,就是不可能同時保存于兩者之中驯妄,只能擇一而存荷并,保存在該緩存池中的是尚未完成創(chuàng)建,而被注入到其他Bean中的Bean實例青扔,可以說該緩存就是一個中間緩存(或者叫過程緩存)源织,只在當(dāng)將該BeanName對應(yīng)的原生Bean(處于創(chuàng)建中池)注入到另一個bean實例中后,將其添加到該緩存中微猖,這個緩存中保存的永遠(yuǎn)是半成品的bean實例谈息,當(dāng)Bean實例最終完成創(chuàng)建后會從此緩存中移除,轉(zhuǎn)移到singletonObjects緩存中保存凛剥。
  • registeredSingletons:已注冊的單例緩存池黎茎,用于保存已完成創(chuàng)建的Bean實例的beanName,是Set(此緩存未涉及)当悔。
  • singletonsCurrentlyInCreation:創(chuàng)建中池傅瞻,保存處于創(chuàng)建中的單例bean的BeanName踢代,是Set,在這個bean實例開始創(chuàng)建時添加到池中嗅骄,而來Bean實例創(chuàng)建完成之后從池中移除胳挎。

當(dāng)存在循環(huán)依賴的情況時,比如之前的情況:A依賴B溺森,B又依賴A的情況慕爬,這種情況下,首先要創(chuàng)建A實例屏积,將其beanName添加到singletonsCurrentlyInCreation池医窿,然后調(diào)用A的構(gòu)造器創(chuàng)建A的原生實例,并將其ObjectFactory添加到singletonFactories緩存中炊林,然后處理依賴注入(B實例)姥卢,發(fā)現(xiàn)B實例不存在且也不在singletonsCurrentlyInCreation池中,表示Bean實例尚未進(jìn)行創(chuàng)建渣聚,那么下一步開始創(chuàng)建B實例独榴,將其beanName添加到singletonsCurrentlyInCreation池,然后調(diào)用B的構(gòu)造器創(chuàng)建A的原生實例奕枝,并將其ObjectFactory添加到singletonFactories緩存中棺榔,再然后處理依賴注入(A實例),發(fā)現(xiàn)A實例尚未創(chuàng)建完成隘道,但在singletonsCurrentlyInCreation池中發(fā)現(xiàn)了A實例的beanName症歇,說明A實例正處于創(chuàng)建中,這時表示出現(xiàn)循環(huán)依賴谭梗,Spring會將singletonFactories緩存中獲取對應(yīng)A的beanName的ObjectFactory中g(shù)etObject方法返回的Bean實例注入到B中忘晤,來完成B實例的創(chuàng)建步驟,同時也會將A的Bean實例添加到earlySingletonObjects緩存中默辨,表示A實例是一個提前暴露的Bean實例德频,B實例創(chuàng)建完畢之后需要將B的原生實例從singletonFactories緩存中移除,并將完整實例添加到SingletonObjects緩存中(當(dāng)然earlySingletonObjects中也不能存在)缩幸,并且將其beanName從singletonsCurrentlyInCreation池中移除(表示B實例完全創(chuàng)建完畢)壹置。然后將B實例注入到A實例中來完成A實例的創(chuàng)建,最后同樣將A的原生實例從earlySingletonObjects中移除表谊,完整實例添加到SingletonObjects中钞护,并將A的beanName從創(chuàng)建中池中移除。到此完成A和B兩個單例實例的創(chuàng)建爆办。

了解了上面所述的解決方案之后难咕,我們可以明白針對構(gòu)造器實現(xiàn)依賴注入的Bean發(fā)生循環(huán)依賴的情況下為什么無法解決。那就是因為,之前提前暴露的前提是創(chuàng)建好原生的Bean實例余佃,原生的Bean實例就是依靠構(gòu)造器創(chuàng)建的暮刃,如果在構(gòu)造器創(chuàng)建Bean的時候就需要注入依賴,而依賴又正處于創(chuàng)建中的話爆土,由于無法暴露ObjectFactory椭懊,而無法解決循環(huán)依賴問題。

另外原型bean的情況步势,Spring根本就不會對原型的Bean添加緩存氧猬,因為添加緩存的目的是為了保證單例Bean的唯一性,但是對于原型坏瘩,就不能緩存了盅抚,如果從緩存獲取的Bean實例,那還是原型模式嗎倔矾?不存在緩存當(dāng)然也就無法實現(xiàn)上面描述的那一系列操作妄均,也就無法解決循環(huán)依賴的問題了。

五破讨、總結(jié)

Spring中的事務(wù)問題歸結(jié)為注入問題丛晦,循環(huán)依賴問題也是注入問題奕纫,有關(guān)注入的問題以后再討論提陶。

Spring之中所有的增強都是依靠AOP實現(xiàn)的,而AOP又是依靠動態(tài)代理實現(xiàn)的匹层,JDK的動態(tài)代理依靠反射技術(shù)實現(xiàn)隙笆,而CGLIB動態(tài)代理依靠字節(jié)碼技術(shù)實現(xiàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末升筏,一起剝皮案震驚了整個濱河市撑柔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌您访,老刑警劉巖铅忿,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異灵汪,居然都是意外死亡檀训,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進(jìn)店門享言,熙熙樓的掌柜王于貴愁眉苦臉地迎上來峻凫,“玉大人,你說我怎么就攤上這事览露∮恚” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長命锄。 經(jīng)常有香客問我堰乔,道長,這世上最難降的妖魔是什么脐恩? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任浩考,我火速辦了婚禮,結(jié)果婚禮上被盈,老公的妹妹穿的比我還像新娘析孽。我一直安慰自己,他們只是感情好只怎,可當(dāng)我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布袜瞬。 她就那樣靜靜地躺著,像睡著了一般身堡。 火紅的嫁衣襯著肌膚如雪邓尤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天贴谎,我揣著相機與錄音汞扎,去河邊找鬼与涡。 笑死掌腰,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的运嗜。 我是一名探鬼主播仲翎,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼痹扇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了溯香?” 一聲冷哼從身側(cè)響起鲫构,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎玫坛,沒想到半個月后结笨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡湿镀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年炕吸,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肠骆。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡算途,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蚀腿,到底是詐尸還是另有隱情嘴瓤,我是刑警寧澤扫外,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站廓脆,受9級特大地震影響筛谚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜停忿,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一驾讲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧席赂,春花似錦吮铭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至癞揉,卻和暖如春纸肉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背喊熟。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工柏肪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人芥牌。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓烦味,卻偏偏與公主長得像,于是被迫代替她去往敵國和親胳泉。 傳聞我的和親對象是個殘疾皇子拐叉,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,864評論 2 354