@Transactional アノテーションと rollbackFor = Exception.class の違いは何ですか? @Transactional アノテーションが失敗する 3 つの理由と解決策

1. まず、Mysql でデータを準備しました

写真

2. シンプルかつ無作法にテストを開始する

1. 私たちの目的は、delflag を 0 に変更し、単純に SQL を準備することです。

 <update id="test">
        UPDATE tbl_users set delflag='0' where account='admin'
 </update>

2. まずテストしてみます@Transactionalコードは次のとおりです 2/0 が例外をスローすることは誰もが知っています。

  @Override
  @Transactional
    public Ret test(){
        int i = articleMapper.test();
        int a = 2/0;
        if(i > 0){
            ResultUtil.success();
        }
        return ResultUtil.error();
    }

3. テスト i=1 を実行して、更新が成功したことを示します。心配しないで、ブレークポイントまで続けて下に進みましょう

写真4. 予想どおり、実行が 54 行目に達したときにエラーが発生しました。java.lang.ArithmeticException: /by zero

写真ArithmeticException5. 注意深い学生であれば、この例外クラスがRuntimeException継承されていることがわかるでしょう。

@Transactionalデフォルトのロールバック例外は次のとおりです。RuntimeException

写真6. このクラスをクリックして、何が継承されているかRuntimeExceptionを確認しました。RuntimeExceptionException

そして、RuntimeException直前のjava.lang.ArithmeticException例外を含め、すべての例外クラスは基本的に継承されます。

したがって、次のサブクラスによってスローされた例外がRuntimeExceptionロールバックできる限り、RuntimeException@Transactional

写真7. この時点で、データベースの値が正常に変更されたかどうかを確認してみましょう。データはロールバックされており、0 に変更されていないことがわかります。

ここに画像の説明を挿入

@Transactional1. 次に、以下のようにロールオーバーできない例外コードを試しています。

try catchこれを直接使用して例外をキャッチし、キャッチでカスタムException例外をスローします。

@Override
@Transactional
public Ret test() throws Exception {
    
    
    int i = articleMapper.test();

    try {
    
    
        int a = 2 / 0;
    } catch (Exception e) {
    
    
        throw new Exception();
    }
    if (i > 0) {
    
    
        ResultUtil.success();
    }
    return ResultUtil.error();
}

2. ok によって直接スローされた例外は、指定した例外です。java.lang.Exceptionデータベースに行きましょう

ここに画像の説明を挿入

3. データベースは 0 に更新され、@Transactional が例外をロールバックできないことを示します。
ここに画像の説明を挿入

結論は

@Transactionalロールバックのみが可能でありRuntimeExceptionRuntimeException次のサブクラスによってスローされた例外はロールバックできませException

Exceptionロールバック例外をサポートする必要がある場合は、使用してください@Transactional(rollbackFor = Exception.class)

ここで追加、削除、変更する場合は、全員がそれを使用することをお勧めします@Transactional(rollbackFor = Exception.class)




トランザクション障害のシナリオの概要

トランザクション アノテーション アノテーション メソッド修飾子の最初のタイプが非パブリックである場合、@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));操作はロールバックされません。メソッドが public に変更されるとTestServiceImpl#insertTestWrongModifier、トランザクションは通常どおり開かれ、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))操作はロールバックされなかったことがわかりました。

上記の 3 つは、@Transactionalアノテーションが機能しない、または@Transactionalアノテーションが失敗する主な理由です。以下はSpring の@Transactionalアノテーションを組み合わせて、ソースコード分析と@Transactionalアノテーションが機能しない理由を実現します。

@Transactional アノテーションが機能しない原理分析

最初

@Transactionalアノテーション メソッド修飾子が非パブリックの場合、@Transactionalアノテーションは機能しません。ここで分析する理由は、@Transactional動的プロキシ実装に基づいているためです。@Transactional実装方法はアノテーション実装の原理で分析されます。Bean の初期化のプロセスでは、アノテーション付きの Bean インスタンスに対してプロキシオブジェクト@Transactionalが作成されます。@TransactionalSpring Scanning のアノテーション情報です。残念ながらソースコードには反映されています。@Transactionalマークされたメソッドの修飾子が public でない場合、デフォルトのメソッドの情報は@Transactional空であるため、Bean またはプロキシに対してプロキシ オブジェクトは作成されませんメソッドの呼び出しは行われません。

@Transactionalアノテーション実装の原理では、Bean がプロキシ オブジェクトを作成するかどうかを判断する方法が紹介されており、その一般的なロジックは次のとおりです。BeanFactoryTransactionAttributeSourceAdvisorSpringに従ってaopポイントカットインスタンスを作成し、現在のBeanクラスのメソッドオブジェクトを走査し、メソッドのアノテーション情報が含まれているかどうかを判定し@Transactional、Beanのメソッドにアノテーション情報が含まれていればその@Transactionalに適応するBeanFactoryTransactionAttributeSourceAdvisor-カッティングポイント。プロキシ オブジェクトを作成する必要があります。その後、プロキシ ロジックがトランザクションの開始ロジックと終了ロジックを管理します。

Springのソースコードでは、Bean作成処理をインターセプトしてBean適応のカットポイントを探す際に、メソッド上の@情報を見つけることが目的であり、あればカットポイントであることを意味する以下のメソッドを使用している。TransactionalBeanに適用 (canApply) できますBeanFactoryTransactionAttributeSourceAdvisor

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;  
   }  
   //后面省略.......  
 }  

プロキシ オブジェクトが作成されていません

したがって、すべてのメソッドの修飾子が非パブリックである場合、プロキシ オブジェクトは作成されません。冒頭のテストコードを例に、通常のモディファイアのtestServiceが下の図のcglibで作成されたプロキシオブジェクトである場合を考えてみましょう。

写真

クラス内のメソッドが非パブリックである場合、そのメソッドはプロキシ オブジェクトにはなりません。

プロキシ呼び出しはありません

次のコードに示すような状況を考えてみましょう。どちらのメソッドにも@Transactionalアノテーションが付けられていますが、一方にはパブリック修飾子があり、もう一方にはパブリック修飾子がありません。この場合、パブリック修飾子を持つアノテーション メソッドが少なくとも 1 つあるため、プロキシ オブジェクトが作成されることが予測できます@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次のようなロジックがあり、その目的は、必要な現在のメソッド アダプテーションの AOP ロジックを取得することです。現在のプロキシ オブジェクトによって実行されます。

List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);  

@Transactional アノテーションが AOP ロジック プロセスを見つけるためにも、同様に 1 回実行されます

AbstractFallbackTransactionAttributeSource#getTransactionAttribute
  • AbstractFallbackTransactionAttributeSource#computeTransactionAttribute

つまり、メソッドのアノテーション情報を見つける必要があり@Transactional、アノテーション情報が無い場合には、@Transactionalエージェントに対応するエージェントロジックは実行されず、メソッドが直接実行されることになる。注釈プロキシ ロジックがないと@Transactional、トランザクションを開くことができません。これについては、前の記事でも説明しました。

二番目

クラス内でマークされたメソッドを呼び出すには、クラス内でメソッドを呼び出します@Transactionalこの場合、トランザクションは開かれません。

最初のタイプを詳細に分析した後、この状況でトランザクション管理が有効にならない理由も推測されるはずです。

トランザクション管理は動的プロキシオブジェクトのプロキシロジックに基づいて実装されているため、クラス内のトランザクションメソッドをクラス内で呼び出す場合、トランザクションメソッドを呼び出す処理はプロキシオブジェクト経由ではなく、this経由で直接呼び出されます。オブジェクトを使用してメソッドを呼び出す場合、バイパスされるプロキシ オブジェクトにはプロキシ ロジックが含まれていてはなりません。

実際にはこのようにプレイすることができ、内部呼び出しによってトランザクションの開始を実現することもできます。

@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_43842093/article/details/131605760