>>号外:關注“Java精選”公衆号,菜單欄->聚合->幹貨分享,回複關鍵詞領取視訊資料、開源項目。
Transactional失效場景介紹
第一種
Transactional注解标注方法修飾符為非public時,@Transactional注解将會不起作用。例如以下代碼。
定義一個錯誤的@Transactional标注實作,修飾一個預設通路符的方法
/**
* @author zhoujy
**/
@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
**/
@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))操作将會儲存到資料庫不會進行復原。
第三種
事務方法内部捕捉了異常,沒有抛出新的異常,導緻事務操作不會進行復原。示例代碼如下。
/**
* @author zhoujy
**/
@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();
}
}
運作測試用例發現,雖然抛出異常,但是異常被捕捉了,沒有抛出到方法 外, testMapper.insert(new Test(210,20,30))操作并沒有復原。
以上三種就是@Transactional注解不起作用,@Transactional注解失效的主要原因。下面結合spring中對于@Transactional的注解實作源碼分析為何導緻@Transactional注解不起作用。
@Transactional注解不起作用原理分析
第一種
@Transactional
注解标注方法修飾符為非public時,
@Transactional
注解将會不起作用。這裡分析 的原因是,
@Transactional
是基于動态代理實作的,
@Transactional
注解實作原理中分析了實作方法,在bean初始化過程中,對含有
@Transactional
标注的bean執行個體建立代理對象,這裡就存在一個spring掃描
@Transactional
注解資訊的過程,不幸的是源碼中展現,标注@Transactional的方法如果修飾符不是public,那麼就預設方法的
@Transactional
資訊為空,那麼将不會對bean進行代理對象建立或者不會對方法進行代理調用
@Transactional
注解實作原理中,介紹了如何判定一個bean是否建立代理對象,大概邏輯是。根據spring建立好一個aop切點
BeanFactoryTransactionAttributeSourceAdvisor
執行個體,周遊目前bean的class的方法對象,判斷方法上面的注解資訊是否包含
@Transactional
,如果bean任何一個方法包含
@Transactional
注解資訊,那麼就是适配這個BeanFactoryTransactionAttributeSourceAdvisor切點。則需要建立代理對象,然後代理邏輯為我們管理事務開閉邏輯。
spring源碼中,在攔截bean的建立過程,尋找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;
}
//後面省略.......
}
不建立代理對象
是以,如果所有方法上的修飾符都是非public的時候,那麼将不會建立代理對象。以一開始的測試代碼為例,如果正常的修飾符的testService是下面圖檔中的,經過cglib建立的代理對象。
如果class中的方法都是非public的那麼将不是代理對象。
不進行代理調用
考慮一種情況,如下面代碼所示。兩個方法都被@Transactional注解标注,但是一個有public修飾符一個沒有,那麼這種情況我們可以預見的話,一定會建立代理對象,因為至少有一個public修飾符的@Transactional注解标注方法。
建立了代理對象,insertTestWrongModifier就會開啟事務嗎?答案是不會。
/**
* @author zhoujy
**/
@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));
}
}
原因是在動态代理對象進行代理邏輯調用時,在cglib建立的代理對象的攔截函數中
CglibAopProxy.DynamicAdvisedInterceptor#intercept
,有一個邏輯如下,目的是擷取目前被代理對象的目前需要執行的method适配的aop邏輯。
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
而針對@Transactional注解查找aop邏輯過程,相似地,也是執行一次
AbstractFallbackTransactionAttributeSource#getTransactionAttribute
-
AbstractFallbackTransactionAttributeSource#computeTransactionAttribute
也就是說還需要找一個方法上的@Transactional注解資訊,沒有的話就不執行代理@Transactional對應的代理邏輯,直接執行方法。沒有了@Transactional注解代理邏輯,就無法開啟事務,這也是上一篇已經講到的。
第二種
在類内部調用調用類内部@Transactional标注的方法。這種情況下也會導緻事務不開啟。
經過對第一種的詳細分析,對這種情況為何不開啟事務管理,原因應該也能猜到;
既然事務管理是基于動态代理對象的代理邏輯實作的,那麼如果在類内部調用類内部的事務方法,這個調用事務方法的過程并不是通過代理對象來調用的,而是直接通過this對象來調用方法,繞過的代理對象,肯定就是沒有代理邏輯了。
其實我們可以這樣玩,内部調用也能實作開啟事務,代碼如下。
/**
* @author zhoujy
**/
@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();
}
}
上面就是使用了代理對象進行事務調用,是以能夠開啟事務管理,但是實際操作中,沒人會閑的蛋疼這樣子玩~
第三種
事務方法内部捕捉了異常,沒有抛出新的異常,導緻事務操作不會進行復原。
這種的話,可能我們比較常見,問題就出在代理邏輯中,我們先看看源碼裡賣弄動态代理邏輯是如何為我們管理事務的。
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.
//反射調用業務方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
//異常時,在catch邏輯中復原事務
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
//送出事務
commitTransactionAfterReturning(txInfo);
return retVal;
}
else {
//....................
}
}
是以看了上面的代碼就一目了然了,事務想要復原,必須能夠在這裡捕捉到異常才行,如果異常中途被捕捉掉,那麼事務将不會復原。
總結了以上幾種情況。
作者:一撸向北
blog.csdn.net/qq_20597727/article/details/84900994
往期精選 點選标題可跳轉
放棄 JDK8 中 StringBuilder,使用 StringJoiner 輔助類,真香!
面試時這樣回答 Java 應用性能調優,回報是更多 Money!
面試官問:你說一說 HashMap 是如何解決 hash 沖突的?
記錄 Java 面試中遇到的 http請求、消息處理、線程池 3 個問題及感悟!
美團面試:說一說 ThreadLocal 原理?網友:難道面試不看源碼不行?
如何了解 Java 開發中 Cookie、Session、Token、JWT 登入認證授權,它們有什麼差別?
用追女孩子的故事通俗解釋 23 種設計模式,看完秒懂!
Spring 架構 @Component、@Service 等注解是如何被解析的?
從零開始實作 Spring Boot 簡易讀寫分離,其實也不難嘛!
Linux 中部署線上環境 Java 應用排查問題時常用指令彙總,值得收藏!
為什麼 MySQL 唯一索引會導緻死鎖,“有心殺賊,無力回天”?
點個贊,就知道你“在看”!