SpringBoot內(nèi)部調(diào)用事務(wù)不起作用問題的解決方案

在做業(yè)務(wù)開發(fā)時犯眠,遇到了一個事務(wù)不起作用的問題。大概流程是這樣的症革,方法內(nèi)部的定時任務(wù)調(diào)用了一個帶事務(wù)的方法筐咧,失敗后事務(wù)沒有回滾。查閱資料后噪矛,問題得到解決量蕊,記錄下來分享給大家。

Transactional失效場景介紹

第一種 Transactional注解標(biāo)注方法修飾符為非public時艇挨,@Transactional注解將會不起作用残炮。例如以下代碼。

定義一個錯誤的@Transactional標(biāo)注實現(xiàn)缩滨,修飾一個默認訪問符的方法

/**

*@authorzhoujy

**/

@Component

publicclassTestServiceImpl{

@Resource

TestMapper?testMapper;

@Transactional

voidinsertTestWrongModifier(){

intre?=?testMapper.insert(newTest(10,20,30));

if(re?>0)?{

thrownewNeedToInterceptException("need?intercept");

}

testMapper.insert(newTest(210,20,30));

}

}

在同一個包內(nèi)势就,新建調(diào)用對象,進行訪問脉漏。

@Component

publicclassInvokcationService{

@Resource

privateTestServiceImpl?testService;

publicvoidinvokeInsertTestWrongModifier(){

//調(diào)用@Transactional標(biāo)注的默認訪問符方法

testService.insertTestWrongModifier();

}

}

測試用例

@RunWith(SpringRunner.class)

@SpringBootTest

publicclassDemoApplicationTests

{

@Resource

InvokcationService?invokcationService;

@Test

publicvoidtestInvoke(){

invokcationService.invokeInsertTestWrongModifier();

}

}

以上的訪問方式苞冯,導(dǎo)致事務(wù)沒開啟,因此在方法拋出異常時侧巨,testMapper.insert(new Test(10,20,30));操作不會進行回滾舅锄。如果TestServiceImpl#insertTestWrongModifier方法改為public的話將會正常開啟事務(wù),testMapper.insert(new Test(10,20,30));將會進行回滾司忱。

第二種

在類內(nèi)部調(diào)用調(diào)用類內(nèi)部@Transactional標(biāo)注的方法皇忿。這種情況下也會導(dǎo)致事務(wù)不開啟畴蹭。示例代碼如下。

設(shè)置一個內(nèi)部調(diào)用

/**

*@authorzhoujy

**/

@Component

publicclassTestServiceImplimplementsTestService{

@Resource

TestMapper?testMapper;

@Transactional

publicvoidinsertTestInnerInvoke(){

//正常public修飾符的事務(wù)方法

intre?=?testMapper.insert(newTest(10,20,30));

if(re?>0)?{

thrownewNeedToInterceptException("need?intercept");

}

testMapper.insert(newTest(210,20,30));

}

publicvoidtestInnerInvoke(){

//類內(nèi)部調(diào)用@Transactional標(biāo)注的方法鳍烁。

insertTestInnerInvoke();

}

}

測試用例叨襟。

@RunWith(SpringRunner.class)

@SpringBootTest

publicclassDemoApplicationTests

{

@Resource

TestServiceImpl?testService;

/**

*?測試內(nèi)部調(diào)用@Transactional標(biāo)注方法

*/

@Test

publicvoidtestInnerInvoke(){

//測試外部調(diào)用事務(wù)方法是否正常

//testService.insertTestInnerInvoke();

//測試內(nèi)部調(diào)用事務(wù)方法是否正常

testService.testInnerInvoke();

}

}

上面就是使用的測試代碼,運行測試知道老翘,外部調(diào)用事務(wù)方法能夠征程開啟事務(wù)芹啥,testMapper.insert(new Test(10,20,30))操作將會被回滾;

然后運行另外一個測試用例铺峭,調(diào)用一個方法在類內(nèi)部調(diào)用內(nèi)部被@Transactional標(biāo)注的事務(wù)方法墓怀,運行結(jié)果是事務(wù)不會正常開啟,testMapper.insert(new Test(10,20,30))操作將會保存到數(shù)據(jù)庫不會進行回滾卫键。

第三種

事務(wù)方法內(nèi)部捕捉了異常傀履,沒有拋出新的異常,導(dǎo)致事務(wù)操作不會進行回滾莉炉。示例代碼如下钓账。

/**

*@authorzhoujy

**/

@Component

publicclassTestServiceImplimplementsTestService{

@Resource

TestMapper?testMapper;

@Transactional

publicvoidinsertTestCatchException(){

try{

intre?=?testMapper.insert(newTest(10,20,30));

if(re?>0)?{

//運行期間拋異常

thrownewNeedToInterceptException("need?intercept");

}

testMapper.insert(newTest(210,20,30));

}catch(Exception?e){

System.out.println("i?catch?exception");

}

}

}

測試用例代碼如下。

@RunWith(SpringRunner.class)

@SpringBootTest

publicclassDemoApplicationTests

{

@Resource

TestServiceImpl?testService;

@Test

publicvoidtestCatchException(){

testService.insertTestCatchException();

}

}

運行測試用例發(fā)現(xiàn)絮宁,雖然拋出異常梆暮,但是異常被捕捉了,沒有拋出到方法 外绍昂, testMapper.insert(new Test(210,20,30))操作并沒有回滾啦粹。

以上三種就是@Transactional注解不起作用,@Transactional注解失效的主要原因窘游。下面結(jié)合spring中對于@Transactional的注解實現(xiàn)源碼分析為何導(dǎo)致@Transactional注解不起作用唠椭。

@Transactional注解不起作用原理分析

第一種

@Transactional注解標(biāo)注方法修飾符為非public時,@Transactional注解將會不起作用忍饰。這里分析 的原因是贪嫂,@Transactional是基于動態(tài)代理實現(xiàn)的,@Transactional注解實現(xiàn)原理中分析了實現(xiàn)方法艾蓝,在bean初始化過程中力崇,對含有@Transactional標(biāo)注的bean實例創(chuàng)建代理對象,這里就存在一個spring掃描@Transactional注解信息的過程赢织,不幸的是源碼中體現(xiàn)餐曹,標(biāo)注@Transactional的方法如果修飾符不是public,那么就默認方法的@Transactional信息為空敌厘,那么將不會對bean進行代理對象創(chuàng)建或者不會對方法進行代理調(diào)用

@Transactional注解實現(xiàn)原理中台猴,介紹了如何判定一個bean是否創(chuàng)建代理對象,大概邏輯是。根據(jù)spring創(chuàng)建好一個aop切點BeanFactoryTransactionAttributeSourceAdvisor實例饱狂,遍歷當(dāng)前bean的class的方法對象曹步,判斷方法上面的注解信息是否包含@Transactional,如果bean任何一個方法包含@Transactional注解信息休讳,那么就是適配這個BeanFactoryTransactionAttributeSourceAdvisor切點讲婚。則需要創(chuàng)建代理對象,然后代理邏輯為我們管理事務(wù)開閉邏輯俊柔。

spring源碼中筹麸,在攔截bean的創(chuàng)建過程,尋找bean適配的切點時雏婶,運用到下面的方法物赶,目的就是尋找方法上面的@Transactional信息,如果有留晚,就表示切點BeanFactoryTransactionAttributeSourceAdvisor能夠應(yīng)用(canApply)到bean中酵紫,

AopUtils#canApply(org.springframework.aop.Pointcut, java.lang.Class<?>, boolean)

publicstaticbooleancanApply(Pointcut?pc,?Class?targetClass,booleanhasIntroductions){

Assert.notNull(pc,"Pointcut?must?not?be?null");

if(!pc.getClassFilter().matches(targetClass))?{

returnfalse;

}

MethodMatcher?methodMatcher?=?pc.getMethodMatcher();

if(methodMatcher?==?MethodMatcher.TRUE)?{

//?No?need?to?iterate?the?methods?if?we're?matching?any?method?anyway...

returntrue;

}

IntroductionAwareMethodMatcher?introductionAwareMethodMatcher?=null;

if(methodMatcherinstanceofIntroductionAwareMethodMatcher)?{

introductionAwareMethodMatcher?=?(IntroductionAwareMethodMatcher)?methodMatcher;

}

//遍歷class的方法對象

Set>?classes?=newLinkedHashSet>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));

classes.add(targetClass);

for(Class?clazz?:?classes)?{

Method[]?methods?=?ReflectionUtils.getAllDeclaredMethods(clazz);

for(Method?method?:?methods)?{

if((introductionAwareMethodMatcher?!=null&&

introductionAwareMethodMatcher.matches(method,?targetClass,?hasIntroductions))?||

//適配查詢方法上的@Transactional注解信息??

methodMatcher.matches(method,?targetClass))?{

returntrue;

}

}

}

returnfalse;

}

我們可以在上面的方法打斷點,一步一步調(diào)試跟蹤代碼错维,最終上面的代碼還會調(diào)用如下方法來判斷奖地。在下面的方法上斷點,回頭看看方法調(diào)用堆棧也是不錯的方式跟蹤赋焕。

AbstractFallbackTransactionAttributeSource#getTransactionAttribute

AbstractFallbackTransactionAttributeSource#computeTransactionAttribute

protectedTransactionAttributecomputeTransactionAttribute(Method?method,?Class<?>?targetClass){

//?Don't?allow?no-public?methods?as?required.

//非public?方法参歹,返回@Transactional信息一律是null

if(allowPublicMethodsOnly()?&&?!Modifier.isPublic(method.getModifiers()))?{

returnnull;

}

//后面省略.......

}

不創(chuàng)建代理對象

所以,如果所有方法上的修飾符都是非public的時候隆判,那么將不會創(chuàng)建代理對象犬庇。以一開始的測試代碼為例,如果正常的修飾符的testService是下面圖片中的蜜氨,經(jīng)過cglib創(chuàng)建的代理對象。

如果class中的方法都是非public的那么將不是代理對象捎泻。

不進行代理調(diào)用

考慮一種情況飒炎,如下面代碼所示。兩個方法都被@Transactional注解標(biāo)注笆豁,但是一個有public修飾符一個沒有郎汪,那么這種情況我們可以預(yù)見的話,一定會創(chuàng)建代理對象闯狱,因為至少有一個public修飾符的@Transactional注解標(biāo)注方法煞赢。

創(chuàng)建了代理對象,insertTestWrongModifier就會開啟事務(wù)嗎哄孤?答案是不會照筑。

/**

*@authorzhoujy

**/

@Component

publicclassTestServiceImplimplementsTestService{

@Resource

TestMapper?testMapper;

@Override

@Transactional

publicvoidinsertTest(){

intre?=?testMapper.insert(newTest(10,20,30));

if(re?>0)?{

thrownewNeedToInterceptException("need?intercept");

}

testMapper.insert(newTest(210,20,30));

}

@Transactional

voidinsertTestWrongModifier(){

intre?=?testMapper.insert(newTest(10,20,30));

if(re?>0)?{

thrownewNeedToInterceptException("need?intercept");

}

testMapper.insert(newTest(210,20,30));

}

}

原因是在動態(tài)代理對象進行代理邏輯調(diào)用時,在cglib創(chuàng)建的代理對象的攔截函數(shù)中CglibAopProxy.DynamicAdvisedInterceptor#intercept,有一個邏輯如下凝危,目的是獲取當(dāng)前被代理對象的當(dāng)前需要執(zhí)行的method適配的aop邏輯波俄。

List?chain?=this.advised.getInterceptorsAndDynamicInterceptionAdvice(method,?targetClass);

而針對@Transactional注解查找aop邏輯過程,相似地蛾默,也是執(zhí)行一次

AbstractFallbackTransactionAttributeSource#getTransactionAttribute

AbstractFallbackTransactionAttributeSource#computeTransactionAttribute

也就是說還需要找一個方法上的@Transactional注解信息懦铺,沒有的話就不執(zhí)行代理@Transactional對應(yīng)的代理邏輯,直接執(zhí)行方法支鸡。沒有了@Transactional注解代理邏輯冬念,就無法開啟事務(wù),這也是上一篇已經(jīng)講到的牧挣。

第二種

在類內(nèi)部調(diào)用調(diào)用類內(nèi)部@Transactional標(biāo)注的方法急前。這種情況下也會導(dǎo)致事務(wù)不開啟。

經(jīng)過對第一種的詳細分析浸踩,對這種情況為何不開啟事務(wù)管理叔汁,原因應(yīng)該也能猜到;

既然事務(wù)管理是基于動態(tài)代理對象的代理邏輯實現(xiàn)的检碗,那么如果在類內(nèi)部調(diào)用類內(nèi)部的事務(wù)方法据块,這個調(diào)用事務(wù)方法的過程并不是通過代理對象來調(diào)用的,而是直接通過this對象來調(diào)用方法折剃,繞過的代理對象另假,肯定就是沒有代理邏輯了。

其實我們可以這樣玩怕犁,內(nèi)部調(diào)用也能實現(xiàn)開啟事務(wù)边篮,代碼如下。

/**

*@authorzhoujy

**/

@Component

publicclassTestServiceImplimplementsTestService{

@Resource

TestMapper?testMapper;

@Resource

TestServiceImpl?testServiceImpl;

@Transactional

publicvoidinsertTestInnerInvoke(){

intre?=?testMapper.insert(newTest(10,20,30));

if(re?>0)?{

thrownewNeedToInterceptException("need?intercept");

}

testMapper.insert(newTest(210,20,30));

}

publicvoidtestInnerInvoke(){

//內(nèi)部調(diào)用事務(wù)方法

testServiceImpl.insertTestInnerInvoke();

}

}

上面就是使用了代理對象進行事務(wù)調(diào)用奏甫,所以能夠開啟事務(wù)管理戈轿,但是實際操作中,沒人會閑的蛋疼這樣子玩~

第三種

事務(wù)方法內(nèi)部捕捉了異常阵子,沒有拋出新的異常思杯,導(dǎo)致事務(wù)操作不會進行回滾。

這種的話挠进,可能我們比較常見色乾,問題就出在代理邏輯中,我們先看看源碼里賣弄動態(tài)代理邏輯是如何為我們管理事務(wù)的领突。

TransactionAspectSupport#invokeWithinTransaction

代碼如下暖璧。

protectedObjectinvokeWithinTransaction(Method?method,?Class?targetClass,finalInvocationCallback?invocation)

throwsThrowable

{

//?If?the?transaction?attribute?is?null,?the?method?is?non-transactional.

finalTransactionAttribute?txAttr?=?getTransactionAttributeSource().getTransactionAttribute(method,?targetClass);

finalPlatformTransactionManager?tm?=?determineTransactionManager(txAttr);

finalString?joinpointIdentification?=?methodIdentification(method,?targetClass);

if(txAttr?==null||?!(tminstanceofCallbackPreferringPlatformTransactionManager))?{

//?Standard?transaction?demarcation?with?getTransaction?and?commit/rollback?calls.

//開啟事務(wù)

TransactionInfo?txInfo?=?createTransactionIfNecessary(tm,?txAttr,?joinpointIdentification);

Object?retVal?=null;

try{

//?This?is?an?around?advice:?Invoke?the?next?interceptor?in?the?chain.

//?This?will?normally?result?in?a?target?object?being?invoked.

//反射調(diào)用業(yè)務(wù)方法

retVal?=?invocation.proceedWithInvocation();

}

catch(Throwable?ex)?{

//?target?invocation?exception

//異常時,在catch邏輯中回滾事務(wù)

completeTransactionAfterThrowing(txInfo,?ex);

throwex;

}

finally{

cleanupTransactionInfo(txInfo);

}

//提交事務(wù)

commitTransactionAfterReturning(txInfo);

returnretVal;

}

else{

//....................

}

}

所以看了上面的代碼就一目了然了君旦,事務(wù)想要回滾澎办,必須能夠在這里捕捉到異常才行嘲碱,如果異常中途被捕捉掉,那么事務(wù)將不會回滾

場景

我在這里模擬一個場景浮驳,大概的調(diào)用方式就如下面的代碼這樣悍汛。

@Override

@Transactional(rollbackFor = RuntimeException.class)

publicvoidinsertUser(User user) {

??userMapper.insertUser(user);

??thrownewRuntimeException("");

}


/**

?* 內(nèi)部調(diào)用新增方法

?*

?* @param user

?*/

@Override

publicvoidinvokeInsertUser(User user) {

??this.insertUser(user);

}

原因

AOP使用的是動態(tài)代理的機制,它會給類生成一個代理類至会,事務(wù)的相關(guān)操作都在代理類上完成离咐。內(nèi)部方式使用this調(diào)用方式時,使用的是實例調(diào)用奉件,并沒有通過代理類調(diào)用方法宵蛀,所以會導(dǎo)致事務(wù)失效。

解決辦法

方法一 引入自身bean

在類內(nèi)部通過@Autowired將本身bean引入县貌,然后通過調(diào)用自身bean术陶,從而實現(xiàn)使用AOP代理操作。

注入自身bean


@Autowired

@Lazy

privateUserService service;

修改invokeInsertUser方法


/**

?* 解決方法一 在bean中將自己注入進來

?* @param user

?*/

@Override

publicvoidinvokeInsertUser(User user) {

??this.service.insertUser(user);

}

方法二 通過ApplicationContext引入bean

通過ApplicationContext獲取bean煤痕,通過bean調(diào)用內(nèi)部方法梧宫,就使用了bean的代理類。

注入ApplicationContext


@Autowired

ApplicationContext applicationContext;

修改invokeInsertUser方法


/**

?* 解決方法二 通過applicationContext獲取到bean

?* @param user

?*/

@Override

publicvoidinvokeInsertUser(User user) {

??((UserService)applicationContext.getBean("userService")).invokeInsertUser(user);

}

方法三 通過AopContext獲取當(dāng)前類的代理類

通過AopContext獲取當(dāng)前類的代理類摆碉,直接通過代理類調(diào)用方法

在引導(dǎo)類上添加@EnableAspectJAutoProxy(exposeProxy=true)注解

修改invokeInsertUser方法


/**

?* 解決方法三 通過applicationContext獲取到bean

?*

?* @param user

?*/

@Override

publicvoidinvokeInsertUser(User user) {

??((UserService) AopContext.currentProxy()).invokeInsertUser(user);

}

以上就是內(nèi)部方法調(diào)用時塘匣,事務(wù)不起作用的原因及解決辦法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末巷帝,一起剝皮案震驚了整個濱河市忌卤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌楞泼,老刑警劉巖驰徊,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異堕阔,居然都是意外死亡棍厂,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門超陆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來牺弹,“玉大人,你說我怎么就攤上這事侥猬±裕” “怎么了捐韩?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵退唠,是天一觀的道長。 經(jīng)常有香客問我荤胁,道長瞧预,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮垢油,結(jié)果婚禮上盆驹,老公的妹妹穿的比我還像新娘。我一直安慰自己滩愁,他們只是感情好躯喇,可當(dāng)我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著硝枉,像睡著了一般廉丽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上妻味,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天正压,我揣著相機與錄音,去河邊找鬼责球。 笑死焦履,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的雏逾。 我是一名探鬼主播嘉裤,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼校套!你這毒婦竟也來了价脾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤笛匙,失蹤者是張志新(化名)和其女友劉穎侨把,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體妹孙,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡秋柄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蠢正。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片骇笔。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖嚣崭,靈堂內(nèi)的尸體忽然破棺而出笨触,到底是詐尸還是另有隱情,我是刑警寧澤雹舀,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布芦劣,位于F島的核電站,受9級特大地震影響说榆,放射性物質(zhì)發(fā)生泄漏虚吟。R本人自食惡果不足惜寸认,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望串慰。 院中可真熱鬧偏塞,春花似錦、人聲如沸邦鲫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庆捺。三九已至怜姿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間疼燥,已是汗流浹背沧卢。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留醉者,地道東北人但狭。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像撬即,于是被迫代替她去往敵國和親立磁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容