@Transactional注解不起作用的原因分析及解決
Transactional失效場景介紹
第一種
Transactional注解標注方法修飾符為非public時,@Transactional注解將會不起作用。例如以下代碼。
定義一個錯誤的@Transactional標注實現(xiàn),修飾一個默認訪問符的方法
/** * @author zhoujy * @date 2018年12月06日 **/ @Component public class TestServiceImpl { @Resource TestMapper testMapper; @Transactional void insertTestWrongModifier() { int re = testMapper.insert(new Test(10,20,30)); if (re > 0) { throw new NeedToInterceptException("need intercept"); } testMapper.insert(new Test(210,20,30)); } }
在同一個包內,新建調用對象,進行訪問。
@Component public class InvokcationService { @Resource private TestServiceImpl testService; public void invokeInsertTestWrongModifier(){ //調用@Transactional標注的默認訪問符方法 testService.insertTestWrongModifier(); } }
測試用例
@RunWith(SpringRunner.class) @SpringBootTest public class DemoApplicationTests { @Resource InvokcationService invokcationService; @Test public void testInvoke(){ invokcationService.invokeInsertTestWrongModifier(); } }
以上的訪問方式,導致事務沒開啟,因此在方法拋出異常時,testMapper.insert(new Test(10,20,30));操作不會進行回滾。如果TestServiceImpl#insertTestWrongModifier方法改為public的話將會正常開啟事務,testMapper.insert(new Test(10,20,30));將會進行回滾。
第二種
在類內部調用調用類內部@Transactional標注的方法。這種情況下也會導致事務不開啟。示例代碼如下。
設置一個內部調用
/** * @author zhoujy * @date 2018年12月06日 **/ @Component public class TestServiceImpl implements TestService { @Resource TestMapper testMapper; @Transactional public void insertTestInnerInvoke() { //正常public修飾符的事務方法 int re = testMapper.insert(new Test(10,20,30)); if (re > 0) { throw new NeedToInterceptException("need intercept"); } testMapper.insert(new Test(210,20,30)); } public void testInnerInvoke(){ //類內部調用@Transactional標注的方法。 insertTestInnerInvoke(); } }
測試用例。
@RunWith(SpringRunner.class) @SpringBootTest public class DemoApplicationTests { @Resource TestServiceImpl testService; /** * 測試內部調用@Transactional標注方法 */ @Test public void testInnerInvoke(){ //測試外部調用事務方法是否正常 //testService.insertTestInnerInvoke(); //測試內部調用事務方法是否正常 testService.testInnerInvoke(); } }
上面就是使用的測試代碼,運行測試知道,外部調用事務方法能夠征程開啟事務,testMapper.insert(new Test(10,20,30))操作將會被回滾;
然后運行另外一個測試用例,調用一個方法在類內部調用內部被@Transactional標注的事務方法,運行結果是事務不會正常開啟,testMapper.insert(new Test(10,20,30))操作將會保存到數(shù)據(jù)庫不會進行回滾。
第三種
事務方法內部捕捉了異常,沒有拋出新的異常,導致事務操作不會進行回滾。示例代碼如下。
/** * @author zhoujy * @date 2018年12月06日 **/ @Component public class TestServiceImpl implements TestService { @Resource TestMapper testMapper; @Transactional public void insertTestCatchException() { try { int re = testMapper.insert(new Test(10,20,30)); if (re > 0) { //運行期間拋異常 throw new NeedToInterceptException("need intercept"); } testMapper.insert(new Test(210,20,30)); }catch (Exception e){ System.out.println("i catch exception"); } } }
測試用例代碼如下。
@RunWith(SpringRunner.class) @SpringBootTest public class DemoApplicationTests { @Resource TestServiceImpl testService; @Test public void testCatchException(){ testService.insertTestCatchException(); } }
運行測試用例發(fā)現(xiàn),雖然拋出異常,但是異常被捕捉了,沒有拋出到方法 外, testMapper.insert(new Test(210,20,30))操作并沒有回滾。
以上三種就是@Transactional注解不起作用,@Transactional注解失效的主要原因。下面結合spring中對于@Transactional的注解實現(xiàn)源碼分析為何導致@Transactional注解不起作用。
@Transactional注解不起作用原理分析
首先不了解@Transactional注解實現(xiàn)原理的可以看一下另一篇文章,@Transactional注解實現(xiàn)原理,然后下面開始結合源碼分析下面三種情況。
第一種
@Transactional注解標注方法修飾符為非public時,@Transactional注解將會不起作用。這里分析 的原因是,@Transactional是基于動態(tài)代理實現(xiàn)的,@Transactional注解實現(xiàn)原理中分析了實現(xiàn)方法,在bean初始化過程中,對含有@Transactional標注的bean實例創(chuàng)建代理對象,這里就存在一個spring掃描@Transactional注解信息的過程,不幸的是源碼中體現(xiàn),標注@Transactional的方法如果修飾符不是public,那么就默認方法的@Transactional信息為空,那么將不會對bean進行代理對象創(chuàng)建或者不會對方法進行代理調用
@Transactional注解實現(xiàn)原理中,介紹了如何判定一個bean是否創(chuàng)建代理對象,大概邏輯是。根據(jù)spring創(chuàng)建好一個aop切點BeanFactoryTransactionAttributeSourceAdvisor實例,遍歷當前bean的class的方法對象,判斷方法上面的注解信息是否包含@Transactional,如果bean任何一個方法包含@Transactional注解信息,那么就是適配這個BeanFactoryTransactionAttributeSourceAdvisor切點。則需要創(chuàng)建代理對象,然后代理邏輯為我們管理事務開閉邏輯。
spring源碼中,在攔截bean的創(chuàng)建過程,尋找bean適配的切點時,運用到下面的方法,目的就是尋找方法上面的@Transactional信息,如果有,就表示切點BeanFactoryTransactionAttributeSourceAdvisor能狗應用(canApply)到bean中,
AopUtils#canApply(org.springframework.aop.Pointcut, java.lang.Class<?>, boolean)
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) { Assert.notNull(pc, "Pointcut must not be null"); if (!pc.getClassFilter().matches(targetClass)) { return false; } MethodMatcher methodMatcher = pc.getMethodMatcher(); if (methodMatcher == MethodMatcher.TRUE) { // No need to iterate the methods if we're matching any method anyway... return true; } IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; if (methodMatcher instanceof IntroductionAwareMethodMatcher) { introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher; } //遍歷class的方法對象 Set<Class<?>> classes = new LinkedHashSet<Class<?>>(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)) { return true; } } } return false; }
我們可以在上面的方法打斷點,一步一步調試跟蹤代碼,最終上面的代碼還會調用如下方法來判斷。在下面的方法上斷點,回頭看看方法調用堆棧也是不錯的方式跟蹤。
AbstractFallbackTransactionAttributeSource#getTransactionAttribute
AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) { // Don't allow no-public methods as required. //非public 方法,返回@Transactional信息一律是null if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; } //后面省略....... }
不創(chuàng)建代理對象
所以,如果所有方法上的修飾符都是非public的時候,那么將不會創(chuàng)建代理對象。以一開始的測試代碼為例,如果正常的修飾符的testService是下面圖片中的,經(jīng)過cglib創(chuàng)建的代理對象。
如果class中的方法都是非public的那么將不是代理對象。
不進行代理調用
考慮一種情況,如下面代碼所示。兩個方法都被@Transactional注解標注,但是一個有public修飾符一個沒有,那么這種情況我們可以預見的話,一定會創(chuàng)建代理對象,因為至少有一個public修飾符的@Transactional注解標注方法。
創(chuàng)建了代理對象,insertTestWrongModifier就會開啟事務嗎?答案是不會。
/** * @author zhoujy * @date 2018年12月06日 **/ @Component public class TestServiceImpl implements TestService { @Resource TestMapper testMapper; @Override @Transactional public void insertTest() { int re = testMapper.insert(new Test(10,20,30)); if (re > 0) { throw new NeedToInterceptException("need intercept"); } testMapper.insert(new Test(210,20,30)); } @Transactional void insertTestWrongModifier() { int re = testMapper.insert(new Test(10,20,30)); if (re > 0) { throw new NeedToInterceptException("need intercept"); } testMapper.insert(new Test(210,20,30)); } }
原因是在動態(tài)代理對象進行代理邏輯調用時,在cglib創(chuàng)建的代理對象的攔截函數(shù)中CglibAopProxy.DynamicAdvisedInterceptor#intercept,有一個邏輯如下,目的是獲取當前被代理對象的當前需要執(zhí)行的method適配的aop邏輯。
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
而針對@Transactional注解查找aop邏輯過程,相似地,也是執(zhí)行一次
AbstractFallbackTransactionAttributeSource#getTransactionAttribute
AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
也就是說還需要找一個方法上的@Transactional注解信息,沒有的話就不執(zhí)行代理@Transactional對應的代理邏輯,直接執(zhí)行方法。沒有了@Transactional注解代理邏輯,就無法開啟事務,這也是上一篇已經(jīng)講到的。
第二種
在類內部調用調用類內部@Transactional標注的方法。這種情況下也會導致事務不開啟。
經(jīng)過對第一種的詳細分析,對這種情況為何不開啟事務管理,原因應該也能猜到;
既然事務管理是基于動態(tài)代理對象的代理邏輯實現(xiàn)的,那么如果在類內部調用類內部的事務方法,這個調用事務方法的過程并不是通過代理對象來調用的,而是直接通過this對象來調用方法,繞過的代理對象,肯定就是沒有代理邏輯了。
其實我們可以這樣玩,內部調用也能實現(xiàn)開啟事務,代碼如下。
/** * @author zhoujy * @date 2018年12月06日 **/ @Component public class TestServiceImpl implements TestService { @Resource TestMapper testMapper; @Resource TestServiceImpl testServiceImpl; @Transactional public void insertTestInnerInvoke() { int re = testMapper.insert(new Test(10,20,30)); if (re > 0) { throw new NeedToInterceptException("need intercept"); } testMapper.insert(new Test(210,20,30)); } public void testInnerInvoke(){ //內部調用事務方法 testServiceImpl.insertTestInnerInvoke(); } }
上面就是使用了代理對象進行事務調用,所以能夠開啟事務管理,但是實際操作中,沒人會閑的蛋疼這樣子玩~
第三種
事務方法內部捕捉了異常,沒有拋出新的異常,導致事務操作不會進行回滾。
這種的話,可能我們比較常見,問題就出在代理邏輯中,我們先看看源碼里賣弄動態(tài)代理邏輯是如何為我們管理事務的,這個過程我就不多贅述了,大家可以參考其他文章。
TransactionAspectSupport#invokeWithinTransaction
代碼如下。
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation) throws Throwable { // If the transaction attribute is null, the method is non-transactional. final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass); final PlatformTransactionManager tm = determineTransactionManager(txAttr); final String joinpointIdentification = methodIdentification(method, targetClass); if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { // Standard transaction demarcation with getTransaction and commit/rollback calls. //開啟事務 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. //反射調用業(yè)務方法 retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // target invocation exception //異常時,在catch邏輯中回滾事務 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); } //提交事務 commitTransactionAfterReturning(txInfo); return retVal; } else { //.................... } }
所以看了上面的代碼就一目了然了,事務想要回滾,必須能夠在這里捕捉到異常才行,如果異常中途被捕捉掉,那么事務將不會回滾。
總結了以上幾種情況~~~~~~~~~~~~~~~~~~~~~
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
詳解Java異常處理中throw與throws關鍵字的用法區(qū)別
這篇文章主要介紹了詳解Java異常處理中throw與throws關鍵字的用法區(qū)別,這也是Java面試題目中的常客,需要的朋友可以參考下2015-11-11SpringCloud應用idea實現(xiàn)可相互調用的多模塊程序詳解
IDEA 全稱 IntelliJ IDEA,是java編程語言的集成開發(fā)環(huán)境。IntelliJ在業(yè)界被公認為最好的Java開發(fā)工具,尤其在智能代碼助手、代碼自動提示、重構、JavaEE支持、各類版本工具(git、svn等)、JUnit、CVS整合、代碼分析、 創(chuàng)新的GUI設計等方面的功能可以說是超常的2022-07-07SpringBoot整合H2數(shù)據(jù)庫的操作方法
H2是一個Java語言編寫的嵌入式數(shù)據(jù)庫,它不受平臺的限制,同時H2提供了一個十分方便的web控制臺,用于操作和管理數(shù)據(jù)庫內容,本文介紹SpringBoot整合H2數(shù)據(jù)庫的方法,感興趣的朋友一起看看吧2024-01-01swagger添加權限驗證保證API(接口)安全性(兩種方法)
這篇文章主要介紹了swagger添加權限驗證保證API(接口)安全性(兩種方法),本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01