Skip to content

Commit aeb0115

Browse files
committed
Introduce MethodRollbackEvent for @transactional rollbacks
Closes gh-36073
1 parent 0810410 commit aeb0115

File tree

4 files changed

+189
-14
lines changed

4 files changed

+189
-14
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.transaction.interceptor;
18+
19+
import org.aopalliance.intercept.MethodInvocation;
20+
21+
import org.springframework.context.event.MethodFailureEvent;
22+
import org.springframework.transaction.TransactionExecution;
23+
24+
/**
25+
* Event published for every exception encountered that triggers a transaction rollback
26+
* through a proxy-triggered method invocation or a reactive publisher returned from it.
27+
* Can be listened to via an {@code ApplicationListener<MethodRollbackEvent>} bean or
28+
* an {@code @EventListener(MethodRollbackEvent.class)} method.
29+
*
30+
* <p>Note: This event gets published right <i>before</i> the actual transaction rollback.
31+
* As a consequence, the exposed {@link #getTransaction() transaction} reflects the state
32+
* of the transaction right before the rollback.
33+
*
34+
* @author Juergen Hoeller
35+
* @since 7.0.3
36+
* @see TransactionInterceptor
37+
* @see org.springframework.transaction.annotation.Transactional
38+
* @see org.springframework.context.ApplicationListener
39+
* @see org.springframework.context.event.EventListener
40+
*/
41+
@SuppressWarnings("serial")
42+
public class MethodRollbackEvent extends MethodFailureEvent {
43+
44+
private final TransactionExecution transaction;
45+
46+
47+
/**
48+
* Create a new event for the given rolled-back method invocation.
49+
* @param invocation the transactional method invocation
50+
* @param failure the exception encountered that triggered a rollback
51+
* @param transaction the transaction status right before the rollback
52+
*/
53+
public MethodRollbackEvent(MethodInvocation invocation, Throwable failure, TransactionExecution transaction) {
54+
super(invocation, failure);
55+
this.transaction = transaction;
56+
}
57+
58+
59+
/**
60+
* Return the exception encountered.
61+
* <p>This may be an exception thrown by the method or emitted by the
62+
* reactive publisher returned from the method.
63+
*/
64+
@Override
65+
public Throwable getFailure() {
66+
return super.getFailure();
67+
}
68+
69+
/**
70+
* Return the corresponding transaction status.
71+
*/
72+
public TransactionExecution getTransaction() {
73+
return this.transaction;
74+
}
75+
76+
}

spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.springframework.transaction.PlatformTransactionManager;
4545
import org.springframework.transaction.ReactiveTransaction;
4646
import org.springframework.transaction.ReactiveTransactionManager;
47+
import org.springframework.transaction.TransactionExecution;
4748
import org.springframework.transaction.TransactionManager;
4849
import org.springframework.transaction.TransactionStatus;
4950
import org.springframework.transaction.TransactionSystemException;
@@ -371,7 +372,7 @@ public void afterPropertiesSet() {
371372
}
372373
catch (Throwable ex) {
373374
// target invocation exception
374-
completeTransactionAfterThrowing(txInfo, ex);
375+
completeTransactionAfterThrowing(txInfo, invocation, ex);
375376
throw ex;
376377
}
377378
finally {
@@ -389,6 +390,7 @@ public void afterPropertiesSet() {
389390
Throwable cause = ex.getCause();
390391
Assert.state(cause != null, "Cause must not be null");
391392
if (txAttr.rollbackOn(cause)) {
393+
invocation.onRollback(cause, status);
392394
status.setRollbackOnly();
393395
}
394396
}
@@ -398,7 +400,7 @@ public void afterPropertiesSet() {
398400
}
399401
else if (VAVR_PRESENT && VavrDelegate.isVavrTry(retVal)) {
400402
// Set rollback-only in case of Vavr failure matching our rollback rules...
401-
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
403+
retVal = VavrDelegate.evaluateTryFailure(invocation, retVal, txAttr, status);
402404
}
403405
}
404406
}
@@ -419,12 +421,13 @@ else if (VAVR_PRESENT && VavrDelegate.isVavrTry(retVal)) {
419421
Object retVal = invocation.proceedWithInvocation();
420422
if (retVal != null && VAVR_PRESENT && VavrDelegate.isVavrTry(retVal)) {
421423
// Set rollback-only in case of Vavr failure matching our rollback rules...
422-
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
424+
retVal = VavrDelegate.evaluateTryFailure(invocation, retVal, txAttr, status);
423425
}
424426
return retVal;
425427
}
426428
catch (Throwable ex) {
427429
if (txAttr.rollbackOn(ex)) {
430+
invocation.onRollback(ex, status);
428431
// A RuntimeException: will lead to a rollback.
429432
if (ex instanceof RuntimeException runtimeException) {
430433
throw runtimeException;
@@ -691,13 +694,16 @@ protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo)
691694
* @param txInfo information about the current transaction
692695
* @param ex throwable encountered
693696
*/
694-
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
697+
protected void completeTransactionAfterThrowing(
698+
@Nullable TransactionInfo txInfo, InvocationCallback invocation, Throwable ex) {
699+
695700
if (txInfo != null && txInfo.getTransactionStatus() != null) {
696701
if (logger.isTraceEnabled()) {
697702
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
698703
"] after exception: " + ex);
699704
}
700705
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
706+
invocation.onRollback(ex, txInfo.getTransactionStatus());
701707
try {
702708
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
703709
}
@@ -826,7 +832,21 @@ public String toString() {
826832
@FunctionalInterface
827833
protected interface InvocationCallback {
828834

835+
/**
836+
* Invocation adaptation method.
837+
* @see org.aopalliance.intercept.MethodInvocation#proceed()
838+
*/
829839
@Nullable Object proceedWithInvocation() throws Throwable;
840+
841+
/**
842+
* Callback method for rollback-triggering exceptions.
843+
* @param failure the application exception that triggered the rollback
844+
* @param execution the current transaction status
845+
* @since 7.0.3
846+
* @see TransactionAttribute#rollbackOn(Throwable)
847+
*/
848+
default void onRollback(Throwable failure, TransactionExecution execution) {
849+
}
830850
}
831851

832852

@@ -868,9 +888,12 @@ public static boolean isVavrTry(Object retVal) {
868888
return (retVal instanceof Try);
869889
}
870890

871-
public static Object evaluateTryFailure(Object retVal, TransactionAttribute txAttr, TransactionStatus status) {
891+
public static Object evaluateTryFailure(
892+
InvocationCallback invocation, Object retVal, TransactionAttribute txAttr, TransactionStatus status) {
893+
872894
return ((Try<?>) retVal).onFailure(ex -> {
873895
if (txAttr.rollbackOn(ex)) {
896+
invocation.onRollback(ex, status);
874897
status.setRollbackOnly();
875898
}
876899
});
@@ -911,7 +934,7 @@ public Object invokeWithinTransaction(Method method, @Nullable Class<?> targetCl
911934
}
912935
},
913936
this::commitTransactionAfterReturning,
914-
this::completeTransactionAfterThrowing,
937+
(txInfo, ex) -> completeTransactionAfterThrowing(txInfo, invocation, ex),
915938
this::rollbackTransactionOnCancel)
916939
.onErrorMap(this::unwrapIfResourceCleanupFailure))
917940
.contextWrite(TransactionContextManager.getOrCreateContext())
@@ -931,7 +954,7 @@ public Object invokeWithinTransaction(Method method, @Nullable Class<?> targetCl
931954
}
932955
},
933956
this::commitTransactionAfterReturning,
934-
this::completeTransactionAfterThrowing,
957+
(txInfo, ex) -> completeTransactionAfterThrowing(txInfo, invocation, ex),
935958
this::rollbackTransactionOnCancel)
936959
.onErrorMap(this::unwrapIfResourceCleanupFailure))
937960
.contextWrite(TransactionContextManager.getOrCreateContext())
@@ -1003,13 +1026,16 @@ private Mono<Void> rollbackTransactionOnCancel(@Nullable ReactiveTransactionInfo
10031026
return Mono.empty();
10041027
}
10051028

1006-
private Mono<Void> completeTransactionAfterThrowing(@Nullable ReactiveTransactionInfo txInfo, Throwable ex) {
1029+
private Mono<Void> completeTransactionAfterThrowing(
1030+
@Nullable ReactiveTransactionInfo txInfo, InvocationCallback invocation, Throwable ex) {
1031+
10071032
if (txInfo != null && txInfo.getReactiveTransaction() != null) {
10081033
if (logger.isTraceEnabled()) {
10091034
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
10101035
"] after exception: " + ex);
10111036
}
10121037
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
1038+
invocation.onRollback(ex, txInfo.getReactiveTransaction());
10131039
return txInfo.getTransactionManager().rollback(txInfo.getReactiveTransaction()).onErrorMap(ex2 -> {
10141040
logger.error("Application exception overridden by rollback exception", ex);
10151041
if (ex2 instanceof TransactionSystemException systemException) {

spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionInterceptor.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828

2929
import org.springframework.aop.support.AopUtils;
3030
import org.springframework.beans.factory.BeanFactory;
31+
import org.springframework.context.ApplicationEventPublisher;
32+
import org.springframework.context.ApplicationEventPublisherAware;
3133
import org.springframework.transaction.PlatformTransactionManager;
34+
import org.springframework.transaction.TransactionExecution;
3235
import org.springframework.transaction.TransactionManager;
3336

3437
/**
@@ -52,7 +55,11 @@
5255
* @see org.springframework.aop.framework.ProxyFactory
5356
*/
5457
@SuppressWarnings("serial")
55-
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
58+
public class TransactionInterceptor extends TransactionAspectSupport
59+
implements MethodInterceptor, ApplicationEventPublisherAware, Serializable {
60+
61+
private @Nullable ApplicationEventPublisher applicationEventPublisher;
62+
5663

5764
/**
5865
* Create a new TransactionInterceptor.
@@ -107,6 +114,11 @@ public TransactionInterceptor(PlatformTransactionManager ptm, Properties attribu
107114
}
108115

109116

117+
@Override
118+
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
119+
this.applicationEventPublisher = applicationEventPublisher;
120+
}
121+
110122
@Override
111123
public @Nullable Object invoke(MethodInvocation invocation) throws Throwable {
112124
// Work out the target class: may be {@code null}.
@@ -115,7 +127,27 @@ public TransactionInterceptor(PlatformTransactionManager ptm, Properties attribu
115127
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
116128

117129
// Adapt to TransactionAspectSupport's invokeWithinTransaction...
118-
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
130+
return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
131+
@Override
132+
public @Nullable Object proceedWithInvocation() throws Throwable {
133+
return invocation.proceed();
134+
}
135+
@Override
136+
public void onRollback(Throwable failure, TransactionExecution execution) {
137+
MethodRollbackEvent event = new MethodRollbackEvent(invocation, failure, execution);
138+
logger.trace(event, failure);
139+
if (applicationEventPublisher != null) {
140+
try {
141+
applicationEventPublisher.publishEvent(event);
142+
}
143+
catch (Throwable ex) {
144+
if (logger.isWarnEnabled()) {
145+
logger.warn("Failed to publish " + event, ex);
146+
}
147+
}
148+
}
149+
}
150+
});
119151
}
120152

121153

spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616

1717
package org.springframework.transaction.annotation;
1818

19+
import java.lang.reflect.Method;
20+
import java.util.ArrayList;
1921
import java.util.Collection;
22+
import java.util.List;
2023
import java.util.Map;
2124
import java.util.Properties;
2225

@@ -26,6 +29,7 @@
2629
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
2730
import org.springframework.beans.factory.annotation.Autowired;
2831
import org.springframework.beans.factory.annotation.Qualifier;
32+
import org.springframework.context.ApplicationListener;
2933
import org.springframework.context.ConfigurableApplicationContext;
3034
import org.springframework.context.annotation.AdviceMode;
3135
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@@ -43,8 +47,10 @@
4347
import org.springframework.transaction.TransactionManager;
4448
import org.springframework.transaction.config.TransactionManagementConfigUtils;
4549
import org.springframework.transaction.event.TransactionalEventListenerFactory;
50+
import org.springframework.transaction.interceptor.MethodRollbackEvent;
4651
import org.springframework.transaction.interceptor.TransactionAttribute;
4752
import org.springframework.transaction.testfixture.CallCountingTransactionManager;
53+
import org.springframework.util.ClassUtils;
4854

4955
import static org.assertj.core.api.Assertions.assertThat;
5056
import static org.assertj.core.api.Assertions.assertThatException;
@@ -358,31 +364,55 @@ void gh24502AppliesTransactionFromAnnotatedInterface() {
358364
}
359365

360366
@Test
361-
void gh23473AppliesToRuntimeExceptionOnly() {
362-
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh23473ConfigA.class);
367+
void gh23473AppliesToRuntimeExceptionOnly() throws Exception {
368+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
369+
ctx.register(Gh23473ConfigA.class);
370+
MethodRollbackEventListener listener = new MethodRollbackEventListener();
371+
ctx.addApplicationListener(listener);
372+
ctx.refresh();
363373
TestServiceWithRollback bean = ctx.getBean("testBean", TestServiceWithRollback.class);
364374
CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class);
365375

376+
Method method1 = TestServiceWithRollback.class.getMethod("methodOne");
377+
Method method2 = TestServiceWithRollback.class.getMethod("methodTwo");
366378
assertThatException().isThrownBy(bean::methodOne);
367379
assertThatException().isThrownBy(bean::methodTwo);
368380
assertThat(txManager.begun).isEqualTo(2);
369381
assertThat(txManager.commits).isEqualTo(2);
370382
assertThat(txManager.rollbacks).isEqualTo(0);
383+
assertThat(listener.events).isEmpty();
371384

372385
ctx.close();
373386
}
374387

375388
@Test
376-
void gh23473AppliesRollbackOnAnyException() {
377-
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh23473ConfigB.class);
389+
void gh23473AppliesRollbackOnAnyException() throws Exception {
390+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
391+
ctx.register(Gh23473ConfigB.class);
392+
MethodRollbackEventListener listener = new MethodRollbackEventListener();
393+
ctx.addApplicationListener(listener);
394+
ctx.refresh();
378395
TestServiceWithRollback bean = ctx.getBean("testBean", TestServiceWithRollback.class);
379396
CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class);
380397

398+
Method method1 = TestServiceWithRollback.class.getMethod("methodOne");
399+
Method method2 = TestServiceWithRollback.class.getMethod("methodTwo");
381400
assertThatException().isThrownBy(bean::methodOne);
382401
assertThatException().isThrownBy(bean::methodTwo);
383402
assertThat(txManager.begun).isEqualTo(2);
384403
assertThat(txManager.commits).isEqualTo(0);
385404
assertThat(txManager.rollbacks).isEqualTo(2);
405+
assertThat(listener.events).hasSize(2);
406+
assertThat(listener.events.get(0))
407+
.satisfies(event -> assertThat(event.getMethod()).isEqualTo(method1))
408+
.satisfies(event -> assertThat(event.getFailure()).isExactlyInstanceOf(Exception.class))
409+
.satisfies(event -> assertThat(event.getTransaction().getTransactionName())
410+
.isEqualTo(ClassUtils.getQualifiedMethodName(method1)));
411+
assertThat(listener.events.get(1))
412+
.satisfies(event -> assertThat(event.getMethod()).isEqualTo(method2))
413+
.satisfies(event -> assertThat(event.getFailure()).isExactlyInstanceOf(Exception.class))
414+
.satisfies(event -> assertThat(event.getTransaction().getTransactionName())
415+
.isEqualTo(ClassUtils.getQualifiedMethodName(method2)));
386416

387417
ctx.close();
388418
}
@@ -759,6 +789,17 @@ public void methodTwo() throws Exception {
759789
}
760790

761791

792+
static class MethodRollbackEventListener implements ApplicationListener<MethodRollbackEvent> {
793+
794+
public final List<MethodRollbackEvent> events = new ArrayList<>();
795+
796+
@Override
797+
public void onApplicationEvent(MethodRollbackEvent event) {
798+
this.events.add(event);
799+
}
800+
}
801+
802+
762803
@Configuration
763804
@EnableTransactionManagement
764805
static class Gh23473ConfigA {

0 commit comments

Comments
 (0)