mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Introduce MethodRollbackEvent for @Transactional rollbacks
Closes gh-36073
This commit is contained in:
+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;
|
||||
}
|
||||
|
||||
}
|
||||
+34
-8
@@ -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
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
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
|
||||
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
|
||||
* @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
|
||||
@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
|
||||
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
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
},
|
||||
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
|
||||
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) {
|
||||
|
||||
+34
-2
@@ -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;
|
||||
* @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
|
||||
}
|
||||
|
||||
|
||||
@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
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
+45
-4
@@ -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;
|
||||
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;
|
||||
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 {
|
||||
}
|
||||
|
||||
@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 {
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user