私活接單qq羣:716817407
java
做者:向北web
blog.csdn.net/qq_20597727/article/details/84900994面試
Transactional失效場景介紹
第一種
Transactional註解標註方法修飾符爲非public時,@Transactional註解將會不起做用。例如如下代碼。spring
定義一個錯誤的@Transactional標註實現,修飾一個默認訪問符的方法數據庫
/**
* @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));將會進行回滾。mybatis
第二種
在類內部調用調用類內部@Transactional標註的方法。這種狀況下也會致使事務不開啓。示例代碼以下。app
設置一個內部調用less
/**
* @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))操做將會保存到數據庫不會進行回滾。
第三種
事務方法內部捕捉了異常,沒有拋出新的異常,致使事務操做不會進行回滾。示例代碼以下。
/**
* @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();
}
}
運行測試用例發現,雖然拋出異常,可是異常被捕捉了,沒有拋出到方法 外, testMapper.insert(new Test(210,20,30))操做並無回滾。
以上三種就是@Transactional註解不起做用,@Transactional註解失效的主要緣由。下面結合spring中對於@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就會開啓事務嗎?答案是不會。
/**
* @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));
}
}
緣由是在動態代理對象進行代理邏輯調用時,在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
* @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();
}
}
上面就是使用了代理對象進行事務調用,因此可以開啓事務管理,可是實際操做中,沒人會閒的蛋疼這樣子玩~
第三種
事務方法內部捕捉了異常,沒有拋出新的異常,致使事務操做不會進行回滾。
這種的話,可能咱們比較常見,問題就出在代理邏輯中,咱們先看看源碼裏賣弄動態代理邏輯是如何爲咱們管理事務的,這個過程在個人另外一篇文章有提到。
-
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 {
//....................
}
}
因此看了上面的代碼就一目瞭然了,事務想要回滾,必須可以在這裏捕捉到異常才行,若是異常中途被捕捉掉,那麼事務將不會回滾。
總結了以上幾種狀況
2020-07-29
2020-05-12
2020-07-02
2020-07-23
2020-05-19
2020-04-23
2020-05-29
2020-08-15
2020-08-15
2020-08-14
本文分享自微信公衆號 - Java小白學心理(gh_9a909fa2fb55)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。