Transactional失效場景介紹
第一種
Transactional注解標注方法修飾符為非public時,@Transactional注解將會不起作用。例如以下代碼。
定義一個錯誤的@Transactional標注實現,修飾一個默認訪問符的方法
@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標注的方法。這種情況下也會導致事務不開啟。示例代碼如下。
設置一個內部調用
@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))操作將會保存到數據庫不會進行回滾。
第三種
事務方法內部捕捉了異常,沒有拋出新的異常,導致事務操作不會進行回滾。示例代碼如下。
@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注解實現原理的可以看一下另一篇文章,@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就會開啟事務嗎?答案是不會。
@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