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
ArithmeticException
5. 注意深い学生であれば、この例外クラスがRuntimeException
継承されていることがわかるでしょう。
@Transactional
デフォルトのロールバック例外は次のとおりです。RuntimeException
6. このクラスをクリックして、何が継承されているかRuntimeException
を確認しました。RuntimeException
Exception
そして、RuntimeException
直前のjava.lang.ArithmeticException
例外を含め、すべての例外クラスは基本的に継承されます。
したがって、次のサブクラスによってスローされた例外がRuntimeException
ロールバックできる限り、RuntimeException
@Transactional
7. この時点で、データベースの値が正常に変更されたかどうかを確認してみましょう。データはロールバックされており、0 に変更されていないことがわかります。
@Transactional
1. 次に、以下のようにロールオーバーできない例外コードを試しています。
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
ロールバックのみが可能でありRuntimeException
、RuntimeException
次のサブクラスによってスローされた例外はロールバックできませ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
が作成されます。@Transactional
Spring Scanning のアノテーション情報です。残念ながらソースコードには反映されています。@Transactional
マークされたメソッドの修飾子が public でない場合、デフォルトのメソッドの情報は@Transactional
空であるため、Bean またはプロキシに対してプロキシ オブジェクトは作成されませんメソッドの呼び出しは行われません。
@Transactional
アノテーション実装の原理では、Bean がプロキシ オブジェクトを作成するかどうかを判断する方法が紹介されており、その一般的なロジックは次のとおりです。BeanFactoryTransactionAttributeSourceAdvisor
Springに従ってaopポイントカットインスタンスを作成し、現在のBeanクラスのメソッドオブジェクトを走査し、メソッドのアノテーション情報が含まれているかどうかを判定し@Transactional
、Beanのメソッドにアノテーション情報が含まれていればその点@Transactional
に適応するBeanFactoryTransactionAttributeSourceAdvisor
-カッティングポイント。プロキシ オブジェクトを作成する必要があります。その後、プロキシ ロジックがトランザクションの開始ロジックと終了ロジックを管理します。
Springのソースコードでは、Bean作成処理をインターセプトしてBean適応のカットポイントを探す際に、メソッド上の@情報を見つけることが目的であり、あればカットポイントであることを意味する以下のメソッドを使用している。Transactional
Beanに適用 (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 {
//....................
}
}
したがって、上記のコードを読むと、トランザクションがロールバックしたい場合は、ここで例外をキャッチできる必要があることが一目でわかります。例外が途中でキャッチされた場合、トランザクションはロールバックされません。
以上の状況をまとめます。