Spring @Transactional注解在什么情況下會失效,為什么?


出處:  https://www.cnblogs.com/hunrry/p/9183209.html

     https://www.cnblogs.com/protected/p/6652188.html

 

 

這幾天在項目里面發現我使用@Transactional注解事務之后,拋了異常居然不回滾。后來終於找到了原因。 
如果你也出現了這種情況,可以從下面開始排查。

一、特性

先來了解一下@Transactional注解事務的特性吧,可以更好排查問題

1、service類標簽(一般不建議在接口上)上添加@Transactional,可以將整個類納入spring事務管理,在每個業務方法執行時都會開啟一個事務,不過這些事務采用相同的管理方式。

2、@Transactional 注解只能應用到 public 可見度的方法上。 如果應用在protected、private或者 package可見度的方法上,也不會報錯,不過事務設置不會起作用。

3、默認情況下,Spring會對unchecked異常進行事務回滾;如果是checked異常則不回滾。 
辣么什么是checked異常,什么是unchecked異常

java里面將派生於Error或者RuntimeException(比如空指針,1/0)的異常稱為unchecked異常,其他繼承自java.lang.Exception得異常統稱為Checked Exception,如IOException、TimeoutException等

  辣么再通俗一點:你寫代碼出現的空指針等異常,會被回滾,文件讀寫,網絡出問題,spring就沒法回滾了。然后我教大家怎么記這個,因為很多同學容易弄混,你寫代碼的時候有些IOException我們的編譯器是能夠檢測到的,說以叫checked異常,你寫代碼的時候空指針等死檢測不到的,所以叫unchecked異常。這樣是不是好記一些啦

4、只讀事務: 
@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true) 
只讀標志只在事務啟動時應用,否則即使配置也會被忽略。 
啟動事務會增加線程開銷,數據庫因共享讀取而鎖定(具體跟數據庫類型和事務隔離級別有關)。通常情況下,僅是讀取數據時,不必設置只讀事務而增加額外的系統開銷。

二:事務傳播模式

Propagation枚舉了多種事務傳播模式,部分列舉如下:

  • 1、REQUIRED(默認模式):業務方法需要在一個容器里運行。如果方法運行時,已經處在一個事務中,那么加入到這個事務,否則自己新建一個新的事務。

  • 2、NOT_SUPPORTED:聲明方法不需要事務。如果方法沒有關聯到一個事務,容器不會為他開啟事務,如果方法在一個事務中被調用,該事務會被掛起,調用結束后,原先的事務會恢復執行。

  • 3、REQUIRESNEW:不管是否存在事務,該方法總匯為自己發起一個新的事務。如果方法已經運行在一個事務中,則原有事務掛起,新的事務被創建。

  • 4、 MANDATORY:該方法只能在一個已經存在的事務中執行,業務方法不能發起自己的事務。如果在沒有事務的環境下被調用,容器拋出例外。

  • 5、SUPPORTS:該方法在某個事務范圍內被調用,則方法成為該事務的一部分。如果方法在該事務范圍外被調用,該方法就在沒有事務的環境下執行。

  • 6、NEVER:該方法絕對不能在事務范圍內執行。如果在就拋例外。只有該方法沒有關聯到任何事務,才正常執行。

  • 7、NESTED:如果一個活動的事務存在,則運行在一個嵌套的事務中。如果沒有活動事務,則按REQUIRED屬性執行。它使用了一個單獨的事務,這個事務擁有多個可以回滾的保存點。內部事務的回滾不會對外部事務造成影響。它只對DataSourceTransactionManager事務管理器起效。

 

二:解決Transactional注解不回滾

1、檢查你方法是不是public的

2、你的異常類型是不是unchecked異常 
如果我想check異常也想回滾怎么辦,注解上面寫明異常類型即可

@Transactional(rollbackFor=Exception.class) 

類似的還有norollbackFor,自定義不回滾的異常

3、數據庫引擎要支持事務,如果是MySQL,注意表要使用支持事務的引擎,比如innodb,如果是myisam,事務是不起作用的

4、是否開啟了對注解的解析

<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>

5、spring是否掃描到你這個包,如下是掃描到org.test下面的包

<context:component-scan base-package="org.test" ></context:component-scan>

6、檢查是不是同一個類中的方法調用(如a方法調用同一個類中的b方法) 

  例如:在類C中,方法A調用方法B,B方法被AOP攔截。(可能是方法B上在另外的@Aspect切面定義上了切點/也可能是加了@Transactional事務注解,底層原理也是AOP實現),最終A-》B ,B並不會觸發AOP。

  解決方案:

1.手動獲取代理對象

  1.將當前的代理類暴露給線程使用,以下2種自己選一個實現即可。

    注解實現方案:springboot:啟動類上加注解:@EnableAspectJAutoProxy(exposeProxy=true):

    配置實現方案:<aop:aspectj-autoproxy expose-proxy="true" />

  2.A中調用B:不要直接用this(因為this是目標對象,自然無法實現代理類的增強方法@before等),而是先去嘗試獲取代理類:UserServiceImpl service = AopContext.currentProxy() != null ? (UserService)AopContext.currentProxy() : this;

2.去除AOP切面增強,把切面方法單獨封裝接口方法,在需要的地方調用

 

7、異常是不是被你catch住了

 


 

問題:  為什么方法修飾符不是public就會出現事務注解失效?

  這幾天,同事遇到一個問題,方向Aop 切入Controller,打請求日志,結果調Service層的方法報空指針錯誤,由於是service沒有注入進來。用了 @Autowired@Resource注解都注入不進來。一行一行的檢查代碼,都沒有發現錯誤,后來只能一個方法一個方法的刪除,看到哪里可以運行,結果發現是因為Controller方法是private私有類型的。后來改了成public就可以了。

貌似不能攔截私有方法? 
試了很多次,都失敗了,是不是不行啊? 

我想了一下,因為aop底層是代理, 
jdk是代理接口,私有方法必然不會存在在接口里,所以就不會被攔截到; 
cglib是子類,private的方法照樣不會出現在子類里,也不能被攔截。 

我不是類內部直接調用方法,而是通過維護一個自身實例的代理 

execution(* test.aop.ServiceA.*(..)) 
public class ServiceA {  
  
    private ServiceA  self;  
  
    public void setSelf(ServiceA self) {  
        this.self = self;  
    }  
  
    public String methodA(String str) {  
        System.out.println("methodA: args=" + str);  
        self.methodB("b");  
        return "12345" + str;  
    }  
  
    private String methodB(String str) {  
        System.out.println("methodB: args=" + str);  
        self.methodC("c");  
        return "12345" + str;  
    }  
  
    public String methodC(String str) {  
        System.out.println("methodC: args=" + str);  
        return "12345" + str;  
    }  
} 

是不是這么回事? 
但是stackoverflow上,有人說 it works fine 
http://stackoverflow.com/questions/4402009/aspectj-and-catching-private-or-inner-methods 

execution(public * test.aop.ServiceA.*(..)) 
還有個奇怪的現象,execution里如果不寫權限,那么public protected package的方法都能被攔截到 
如果寫了public,那就只攔截public方法這個沒問題, 
如果寫了protected,他就什么事情都不做,連protected的方法也不攔截。

 

分析

private方法 在Spring使用純Spring AOP(只能攔截public/protected/包)都是無法被攔截的 因為子類無法覆蓋;包級別能被攔截的原因是,如果子類和父類在同一個包中是能覆蓋的。 

在cglib代理情況下, execution(* *(..)) 可以攔截 public/protected/包級別方法(即這些方法都是能代理的)。 

private static boolean isOverridable(Method method, Class targetClass) {  
        if (Modifier.isPrivate(method.getModifiers())) {  
            return false;  
        }  
        if (Modifier.isPublic(method.getModifiers()) || Modifier.isProtected(method.getModifiers())) {  
            return true;  
        }  
        return getPackageName(method.getDeclaringClass()).equals(getPackageName(targetClass));  
    } 

如果想要實現攔截private方法的 可以使用 原生 AspectJ 編譯期/運行期織入。 


引用
如果寫了protected,他就什么事情都不做,連protected的方法也不攔截;這個應該不會


原因基本分析明白了: 

是否能應用增強的判斷代碼如下(org.springframework.aop.support.AopUtils):

public static boolean canApply(Pointcut pc, Class targetClass, boolean hasIntroductions) {  
    if (!pc.getClassFilter().matches(targetClass)) {  
        return false;  
    }  
  
    MethodMatcher methodMatcher = pc.getMethodMatcher();  
    IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;  
    if (methodMatcher instanceof IntroductionAwareMethodMatcher) {  
        introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;  
    }  
  
    Set classes = new HashSet(ClassUtils.getAllInterfacesForClassAsSet(targetClass));  
    classes.add(targetClass);  
    for (Iterator it = classes.iterator(); it.hasNext();) {  
        Class clazz = (Class) it.next();  
        Method[] methods = clazz.getMethods();  
        for (int j = 0; j < methods.length; j++) {  
            if ((introductionAwareMethodMatcher != null &&  
                    introductionAwareMethodMatcher.matches(methods[j], targetClass, hasIntroductions)) ||  
                    methodMatcher.matches(methods[j], targetClass)) {  
                return true;  
            }  
        }  
    }  
  
    return false;  
}  

此處Method[] methods = clazz.getMethods();只能拿到public方法。。 

場景1:execution(* *(..)) 

public class Impl2  {  
      
    protected/public String testAop2() {  
        System.out.println("234");  
        return "1233";  
    }  
}  

因為切入點沒有訪問修飾符,即可以是任意,因此canApply方法能拿到如wait這種public方法,即可以實施代理。 

場景2:execution(public * *(..)) 

public class Impl2  {  
      
    public String testAop2() {  
        System.out.println("234");  
        return "1233";  
    }  
}  

因為攔截public的,因此canApply方法能拿到如wait這種public方法,即可以實施代理。 


場景3:execution(protected * *(..)) 

public class Impl2  {  
      
    protected String testAop2() {  
        System.out.println("234");  
        return "1233";  
    }  
}  

還記得之前說過,在canApply方法中 的 Method[] methods = clazz.getMethods();只能拿到public方法的,因此跟protected訪問修飾符是無法匹配的,所以如果“execution(protected * *(..))” 是 無法代理的。 

這就是為什么execution(protected * *(..))在純Spring AOP環境下不行的原因。 

注,@Transactional注解事務的特殊情況: 

引用
方法的可見度和 @Transactional 
在使用代理的時候,@Transactional 注解應該只被應用到 public 可見度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,系統也不會報錯, 但是這個被注解的方法將不會執行已配置的事務設置。如果你非要注解非公共方法的話,請參考使用AspectJ 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM