如何解決Spring事務(wù)不生效的問題與循環(huán)依賴問題,相信很多沒有經(jīng)驗的人對此束手無策,為此本文總結(jié)了問題出現(xiàn)的原因和解決方法,通過這篇文章希望你能解決這個問題。
創(chuàng)新互聯(lián)建站自2013年起,是專業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項目成都網(wǎng)站建設(shè)、成都網(wǎng)站制作網(wǎng)站策劃,項目實施與項目整合能力。我們以讓每一個夢想脫穎而出為使命,1280元靖安做網(wǎng)站,已為上家服務(wù),為靖安各地企業(yè)和個人服務(wù),聯(lián)系電話:028-86922220
一、提出問題
不知道你是否遇到過這樣的情況,在ssm框架中開發(fā)web引用,或者使用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è)面證明我們提供的方法時有效的。
四、問題引申
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)依賴的問題了。
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)。
看完上述內(nèi)容,你們掌握如何解決Spring事務(wù)不生效的問題與循環(huán)依賴問題的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝各位的閱讀!