Browse Source

Introduce MethodRollbackEvent for @Transactional rollbacks

Closes gh-36073
pull/36054/head
Juergen Hoeller 1 month ago
parent
commit
aeb0115605
  1. 76
      spring-tx/src/main/java/org/springframework/transaction/interceptor/MethodRollbackEvent.java
  2. 42
      spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java
  3. 36
      spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionInterceptor.java
  4. 49
      spring-tx/src/test/java/org/springframework/transaction/annotation/EnableTransactionManagementTests.java

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

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.transaction.interceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.context.event.MethodFailureEvent;
import org.springframework.transaction.TransactionExecution;
/**
* Event published for every exception encountered that triggers a transaction rollback
* through a proxy-triggered method invocation or a reactive publisher returned from it.
* Can be listened to via an {@code ApplicationListener<MethodRollbackEvent>} bean or
* an {@code @EventListener(MethodRollbackEvent.class)} method.
*
* <p>Note: This event gets published right <i>before</i> the actual transaction rollback.
* As a consequence, the exposed {@link #getTransaction() transaction} reflects the state
* of the transaction right before the rollback.
*
* @author Juergen Hoeller
* @since 7.0.3
* @see TransactionInterceptor
* @see org.springframework.transaction.annotation.Transactional
* @see org.springframework.context.ApplicationListener
* @see org.springframework.context.event.EventListener
*/
@SuppressWarnings("serial")
public class MethodRollbackEvent extends MethodFailureEvent {
private final TransactionExecution transaction;
/**
* Create a new event for the given rolled-back method invocation.
* @param invocation the transactional method invocation
* @param failure the exception encountered that triggered a rollback
* @param transaction the transaction status right before the rollback
*/
public MethodRollbackEvent(MethodInvocation invocation, Throwable failure, TransactionExecution transaction) {
super(invocation, failure);
this.transaction = transaction;
}
/**
* Return the exception encountered.
* <p>This may be an exception thrown by the method or emitted by the
* reactive publisher returned from the method.
*/
@Override
public Throwable getFailure() {
return super.getFailure();
}
/**
* Return the corresponding transaction status.
*/
public TransactionExecution getTransaction() {
return this.transaction;
}
}

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

@ -44,6 +44,7 @@ import org.springframework.transaction.NoTransactionException; @@ -44,6 +44,7 @@ import org.springframework.transaction.NoTransactionException;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.ReactiveTransaction;
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.TransactionExecution;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.TransactionSystemException;
@ -371,7 +372,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init @@ -371,7 +372,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
completeTransactionAfterThrowing(txInfo, invocation, ex);
throw ex;
}
finally {
@ -389,6 +390,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init @@ -389,6 +390,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
Throwable cause = ex.getCause();
Assert.state(cause != null, "Cause must not be null");
if (txAttr.rollbackOn(cause)) {
invocation.onRollback(cause, status);
status.setRollbackOnly();
}
}
@ -398,7 +400,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init @@ -398,7 +400,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
}
else if (VAVR_PRESENT && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
retVal = VavrDelegate.evaluateTryFailure(invocation, retVal, txAttr, status);
}
}
}
@ -419,12 +421,13 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init @@ -419,12 +421,13 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
Object retVal = invocation.proceedWithInvocation();
if (retVal != null && VAVR_PRESENT && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
retVal = VavrDelegate.evaluateTryFailure(invocation, retVal, txAttr, status);
}
return retVal;
}
catch (Throwable ex) {
if (txAttr.rollbackOn(ex)) {
invocation.onRollback(ex, status);
// A RuntimeException: will lead to a rollback.
if (ex instanceof RuntimeException runtimeException) {
throw runtimeException;
@ -691,13 +694,16 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init @@ -691,13 +694,16 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
* @param txInfo information about the current transaction
* @param ex throwable encountered
*/
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
protected void completeTransactionAfterThrowing(
@Nullable TransactionInfo txInfo, InvocationCallback invocation, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
invocation.onRollback(ex, txInfo.getTransactionStatus());
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
@ -826,7 +832,21 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init @@ -826,7 +832,21 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
@FunctionalInterface
protected interface InvocationCallback {
/**
* Invocation adaptation method.
* @see org.aopalliance.intercept.MethodInvocation#proceed()
*/
@Nullable Object proceedWithInvocation() throws Throwable;
/**
* Callback method for rollback-triggering exceptions.
* @param failure the application exception that triggered the rollback
* @param execution the current transaction status
* @since 7.0.3
* @see TransactionAttribute#rollbackOn(Throwable)
*/
default void onRollback(Throwable failure, TransactionExecution execution) {
}
}
@ -868,9 +888,12 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init @@ -868,9 +888,12 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
return (retVal instanceof Try);
}
public static Object evaluateTryFailure(Object retVal, TransactionAttribute txAttr, TransactionStatus status) {
public static Object evaluateTryFailure(
InvocationCallback invocation, Object retVal, TransactionAttribute txAttr, TransactionStatus status) {
return ((Try<?>) retVal).onFailure(ex -> {
if (txAttr.rollbackOn(ex)) {
invocation.onRollback(ex, status);
status.setRollbackOnly();
}
});
@ -911,7 +934,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init @@ -911,7 +934,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
}
},
this::commitTransactionAfterReturning,
this::completeTransactionAfterThrowing,
(txInfo, ex) -> completeTransactionAfterThrowing(txInfo, invocation, ex),
this::rollbackTransactionOnCancel)
.onErrorMap(this::unwrapIfResourceCleanupFailure))
.contextWrite(TransactionContextManager.getOrCreateContext())
@ -931,7 +954,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init @@ -931,7 +954,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
}
},
this::commitTransactionAfterReturning,
this::completeTransactionAfterThrowing,
(txInfo, ex) -> completeTransactionAfterThrowing(txInfo, invocation, ex),
this::rollbackTransactionOnCancel)
.onErrorMap(this::unwrapIfResourceCleanupFailure))
.contextWrite(TransactionContextManager.getOrCreateContext())
@ -1003,13 +1026,16 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init @@ -1003,13 +1026,16 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
return Mono.empty();
}
private Mono<Void> completeTransactionAfterThrowing(@Nullable ReactiveTransactionInfo txInfo, Throwable ex) {
private Mono<Void> completeTransactionAfterThrowing(
@Nullable ReactiveTransactionInfo txInfo, InvocationCallback invocation, Throwable ex) {
if (txInfo != null && txInfo.getReactiveTransaction() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
invocation.onRollback(ex, txInfo.getReactiveTransaction());
return txInfo.getTransactionManager().rollback(txInfo.getReactiveTransaction()).onErrorMap(ex2 -> {
logger.error("Application exception overridden by rollback exception", ex);
if (ex2 instanceof TransactionSystemException systemException) {

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

@ -28,7 +28,10 @@ import org.jspecify.annotations.Nullable; @@ -28,7 +28,10 @@ import org.jspecify.annotations.Nullable;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionExecution;
import org.springframework.transaction.TransactionManager;
/**
@ -52,7 +55,11 @@ import org.springframework.transaction.TransactionManager; @@ -52,7 +55,11 @@ import org.springframework.transaction.TransactionManager;
* @see org.springframework.aop.framework.ProxyFactory
*/
@SuppressWarnings("serial")
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
public class TransactionInterceptor extends TransactionAspectSupport
implements MethodInterceptor, ApplicationEventPublisherAware, Serializable {
private @Nullable ApplicationEventPublisher applicationEventPublisher;
/**
* Create a new TransactionInterceptor.
@ -107,6 +114,11 @@ public class TransactionInterceptor extends TransactionAspectSupport implements @@ -107,6 +114,11 @@ public class TransactionInterceptor extends TransactionAspectSupport implements
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
@Override
public @Nullable Object invoke(MethodInvocation invocation) throws Throwable {
// Work out the target class: may be {@code null}.
@ -115,7 +127,27 @@ public class TransactionInterceptor extends TransactionAspectSupport implements @@ -115,7 +127,27 @@ public class TransactionInterceptor extends TransactionAspectSupport implements
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
// Adapt to TransactionAspectSupport's invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
@Override
public @Nullable Object proceedWithInvocation() throws Throwable {
return invocation.proceed();
}
@Override
public void onRollback(Throwable failure, TransactionExecution execution) {
MethodRollbackEvent event = new MethodRollbackEvent(invocation, failure, execution);
logger.trace(event, failure);
if (applicationEventPublisher != null) {
try {
applicationEventPublisher.publishEvent(event);
}
catch (Throwable ex) {
if (logger.isWarnEnabled()) {
logger.warn("Failed to publish " + event, ex);
}
}
}
}
});
}

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

@ -16,7 +16,10 @@ @@ -16,7 +16,10 @@
package org.springframework.transaction.annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Properties;
@ -26,6 +29,7 @@ import org.springframework.aop.support.AopUtils; @@ -26,6 +29,7 @@ import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@ -43,8 +47,10 @@ import org.springframework.transaction.PlatformTransactionManager; @@ -43,8 +47,10 @@ import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.config.TransactionManagementConfigUtils;
import org.springframework.transaction.event.TransactionalEventListenerFactory;
import org.springframework.transaction.interceptor.MethodRollbackEvent;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.testfixture.CallCountingTransactionManager;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
@ -358,31 +364,55 @@ class EnableTransactionManagementTests { @@ -358,31 +364,55 @@ class EnableTransactionManagementTests {
}
@Test
void gh23473AppliesToRuntimeExceptionOnly() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh23473ConfigA.class);
void gh23473AppliesToRuntimeExceptionOnly() throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(Gh23473ConfigA.class);
MethodRollbackEventListener listener = new MethodRollbackEventListener();
ctx.addApplicationListener(listener);
ctx.refresh();
TestServiceWithRollback bean = ctx.getBean("testBean", TestServiceWithRollback.class);
CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class);
Method method1 = TestServiceWithRollback.class.getMethod("methodOne");
Method method2 = TestServiceWithRollback.class.getMethod("methodTwo");
assertThatException().isThrownBy(bean::methodOne);
assertThatException().isThrownBy(bean::methodTwo);
assertThat(txManager.begun).isEqualTo(2);
assertThat(txManager.commits).isEqualTo(2);
assertThat(txManager.rollbacks).isEqualTo(0);
assertThat(listener.events).isEmpty();
ctx.close();
}
@Test
void gh23473AppliesRollbackOnAnyException() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Gh23473ConfigB.class);
void gh23473AppliesRollbackOnAnyException() throws Exception {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(Gh23473ConfigB.class);
MethodRollbackEventListener listener = new MethodRollbackEventListener();
ctx.addApplicationListener(listener);
ctx.refresh();
TestServiceWithRollback bean = ctx.getBean("testBean", TestServiceWithRollback.class);
CallCountingTransactionManager txManager = ctx.getBean(CallCountingTransactionManager.class);
Method method1 = TestServiceWithRollback.class.getMethod("methodOne");
Method method2 = TestServiceWithRollback.class.getMethod("methodTwo");
assertThatException().isThrownBy(bean::methodOne);
assertThatException().isThrownBy(bean::methodTwo);
assertThat(txManager.begun).isEqualTo(2);
assertThat(txManager.commits).isEqualTo(0);
assertThat(txManager.rollbacks).isEqualTo(2);
assertThat(listener.events).hasSize(2);
assertThat(listener.events.get(0))
.satisfies(event -> assertThat(event.getMethod()).isEqualTo(method1))
.satisfies(event -> assertThat(event.getFailure()).isExactlyInstanceOf(Exception.class))
.satisfies(event -> assertThat(event.getTransaction().getTransactionName())
.isEqualTo(ClassUtils.getQualifiedMethodName(method1)));
assertThat(listener.events.get(1))
.satisfies(event -> assertThat(event.getMethod()).isEqualTo(method2))
.satisfies(event -> assertThat(event.getFailure()).isExactlyInstanceOf(Exception.class))
.satisfies(event -> assertThat(event.getTransaction().getTransactionName())
.isEqualTo(ClassUtils.getQualifiedMethodName(method2)));
ctx.close();
}
@ -759,6 +789,17 @@ class EnableTransactionManagementTests { @@ -759,6 +789,17 @@ class EnableTransactionManagementTests {
}
static class MethodRollbackEventListener implements ApplicationListener<MethodRollbackEvent> {
public final List<MethodRollbackEvent> events = new ArrayList<>();
@Override
public void onApplicationEvent(MethodRollbackEvent event) {
this.events.add(event);
}
}
@Configuration
@EnableTransactionManagement
static class Gh23473ConfigA {

Loading…
Cancel
Save