diff --git a/framework-docs/modules/ROOT/pages/data-access/transaction/event.adoc b/framework-docs/modules/ROOT/pages/data-access/transaction/event.adoc
index b4edfd8c4e..8d34460168 100644
--- a/framework-docs/modules/ROOT/pages/data-access/transaction/event.adoc
+++ b/framework-docs/modules/ROOT/pages/data-access/transaction/event.adoc
@@ -57,10 +57,14 @@ attribute of the annotation to `true`.
[NOTE]
====
-`@TransactionalEventListener` only works with thread-bound transactions managed by
-`PlatformTransactionManager`. A reactive transaction managed by `ReactiveTransactionManager`
-uses the Reactor context instead of thread-local attributes, so from the perspective of
-an event listener, there is no compatible active transaction that it can participate in.
+As of 6.1, `@TransactionalEventListener` can work with thread-bound transactions managed by
+`PlatformTransactionManager` as well as reactive transactions managed by `ReactiveTransactionManager`.
+For the former, listeners are guaranteed to see the current thread-bound transaction.
+Since the latter uses the Reactor context instead of thread-local variables, the transaction
+context needs to be included in the published event instance as the event source.
+See the
+{api-spring-framework}/transaction/reactive/TransactionalEventPublisher.html[`TransactionalEventPublisher`]
+javadoc for details.
====
diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java
index 779b73c1c4..30cdd4e4d4 100644
--- a/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java
+++ b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -29,6 +29,7 @@ package org.springframework.context;
* @see org.springframework.context.ApplicationEvent
* @see org.springframework.context.event.ApplicationEventMulticaster
* @see org.springframework.context.event.EventPublicationInterceptor
+ * @see org.springframework.transaction.event.TransactionalApplicationListener
*/
@FunctionalInterface
public interface ApplicationEventPublisher {
@@ -42,8 +43,21 @@ public interface ApplicationEventPublisher {
* or even immediate execution at all. Event listeners are encouraged
* to be as efficient as possible, individually using asynchronous
* execution for longer-running and potentially blocking operations.
+ *
For usage in a reactive call stack, include event publication
+ * as a simple hand-off:
+ * {@code Mono.fromRunnable(() -> eventPublisher.publishEvent(...))}.
+ * As with any asynchronous execution, thread-local data is not going
+ * to be available for reactive listener methods. All state which is
+ * necessary to process the event needs to be included in the event
+ * instance itself.
+ *
For the convenient inclusion of the current transaction context
+ * in a reactive hand-off, consider using
+ * {@link org.springframework.transaction.reactive.TransactionalEventPublisher#publishEvent(Function)}.
+ * For thread-bound transactions, this is not necessary since the
+ * state will be implicitly available through thread-local storage.
* @param event the event to publish
* @see #publishEvent(Object)
+ * @see ApplicationListener#supportsAsyncExecution()
* @see org.springframework.context.event.ContextRefreshedEvent
* @see org.springframework.context.event.ContextClosedEvent
*/
@@ -61,6 +75,11 @@ public interface ApplicationEventPublisher {
* or even immediate execution at all. Event listeners are encouraged
* to be as efficient as possible, individually using asynchronous
* execution for longer-running and potentially blocking operations.
+ *
For the convenient inclusion of the current transaction context
+ * in a reactive hand-off, consider using
+ * {@link org.springframework.transaction.reactive.TransactionalEventPublisher#publishEvent(Object)}.
+ * For thread-bound transactions, this is not necessary since the
+ * state will be implicitly available through thread-local storage.
* @param event the event to publish
* @since 4.2
* @see #publishEvent(ApplicationEvent)
diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListener.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListener.java
index e829fe3042..0ba5818c14 100644
--- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListener.java
+++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListener.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -32,12 +32,13 @@ import org.springframework.lang.Nullable;
* allows you to prioritize that listener amongst other listeners running before or after
* transaction completion.
*
- *
NOTE: Transactional event listeners only work with thread-bound transactions
- * managed by a {@link org.springframework.transaction.PlatformTransactionManager
- * PlatformTransactionManager}. A reactive transaction managed by a
- * {@link org.springframework.transaction.ReactiveTransactionManager ReactiveTransactionManager}
- * uses the Reactor context instead of thread-local variables, so from the perspective of
- * an event listener, there is no compatible active transaction that it can participate in.
+ *
As of 6.1, transactional event listeners can work with thread-bound transactions managed
+ * by a {@link org.springframework.transaction.PlatformTransactionManager} as well as reactive
+ * transactions managed by a {@link org.springframework.transaction.ReactiveTransactionManager}.
+ * For the former, listeners are guaranteed to see the current thread-bound transaction.
+ * Since the latter uses the Reactor context instead of thread-local variables, the transaction
+ * context needs to be included in the published event instance as the event source:
+ * see {@link org.springframework.transaction.reactive.TransactionalEventPublisher}.
*
* @author Juergen Hoeller
* @author Oliver Drotbohm
@@ -60,6 +61,16 @@ public interface TransactionalApplicationListener
return Ordered.LOWEST_PRECEDENCE;
}
+ /**
+ * Transaction-synchronized listeners do not support asynchronous execution,
+ * only their target listener ({@link #processEvent}) potentially does.
+ * @since 6.1
+ */
+ @Override
+ default boolean supportsAsyncExecution() {
+ return false;
+ }
+
/**
* Return an identifier for the listener to be able to refer to it individually.
*
It might be necessary for specific completion callback implementations
diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerAdapter.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerAdapter.java
index 18ce10c3a5..77ab522ba6 100644
--- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerAdapter.java
+++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerAdapter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -22,7 +22,6 @@ import java.util.concurrent.CopyOnWriteArrayList;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
-import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;
/**
@@ -128,11 +127,7 @@ public class TransactionalApplicationListenerAdapter
@Override
public void onApplicationEvent(E event) {
- if (TransactionSynchronizationManager.isSynchronizationActive() &&
- TransactionSynchronizationManager.isActualTransactionActive()) {
- TransactionSynchronizationManager.registerSynchronization(
- new TransactionalApplicationListenerSynchronization<>(event, this, this.callbacks));
- }
+ TransactionalApplicationListenerSynchronization.register(event, this, this.callbacks);
}
}
diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java
index a7830e4761..4b59f70c41 100644
--- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java
+++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerMethodAdapter.java
@@ -25,7 +25,6 @@ import org.springframework.context.event.ApplicationListenerMethodAdapter;
import org.springframework.context.event.EventListener;
import org.springframework.context.event.GenericApplicationListener;
import org.springframework.core.annotation.AnnotatedElementUtils;
-import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;
/**
@@ -87,10 +86,10 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationLi
@Override
public void onApplicationEvent(ApplicationEvent event) {
- if (TransactionSynchronizationManager.isSynchronizationActive() &&
- TransactionSynchronizationManager.isActualTransactionActive()) {
- TransactionSynchronizationManager.registerSynchronization(
- new TransactionalApplicationListenerSynchronization<>(event, this, this.callbacks));
+ if (TransactionalApplicationListenerSynchronization.register(event, this, this.callbacks)) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Registered transaction synchronization for " + event);
+ }
}
else if (this.annotation.fallbackExecution()) {
if (this.annotation.phase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) {
diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerSynchronization.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerSynchronization.java
index d8fc358752..2def91e493 100644
--- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerSynchronization.java
+++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalApplicationListenerSynchronization.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -18,19 +18,21 @@ package org.springframework.transaction.event;
import java.util.List;
+import reactor.core.publisher.Mono;
+
import org.springframework.context.ApplicationEvent;
-import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.core.Ordered;
+import org.springframework.transaction.reactive.TransactionContext;
/**
- * {@link TransactionSynchronization} implementation for event processing with a
+ * {@code TransactionSynchronization} implementations for event processing with a
* {@link TransactionalApplicationListener}.
*
* @author Juergen Hoeller
* @since 5.3
* @param the specific {@code ApplicationEvent} subclass to listen to
*/
-class TransactionalApplicationListenerSynchronization
- implements TransactionSynchronization {
+abstract class TransactionalApplicationListenerSynchronization implements Ordered {
private final E event;
@@ -53,28 +55,11 @@ class TransactionalApplicationListenerSynchronization callback.preProcessEvent(this.event));
try {
this.listener.processEvent(this.event);
@@ -86,4 +71,94 @@ class TransactionalApplicationListenerSynchronization callback.postProcessEvent(this.event, null));
}
+
+ public static boolean register(
+ E event, TransactionalApplicationListener listener,
+ List callbacks) {
+
+ if (org.springframework.transaction.support.TransactionSynchronizationManager.isSynchronizationActive() &&
+ org.springframework.transaction.support.TransactionSynchronizationManager.isActualTransactionActive()) {
+ org.springframework.transaction.support.TransactionSynchronizationManager.registerSynchronization(
+ new PlatformSynchronization<>(event, listener, callbacks));
+ return true;
+ }
+ else if (event.getSource() instanceof TransactionContext txContext) {
+ org.springframework.transaction.reactive.TransactionSynchronizationManager rtsm =
+ new org.springframework.transaction.reactive.TransactionSynchronizationManager(txContext);
+ if (rtsm.isSynchronizationActive() && rtsm.isActualTransactionActive()) {
+ rtsm.registerSynchronization(new ReactiveSynchronization<>(event, listener, callbacks));
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ private static class PlatformSynchronization
+ extends TransactionalApplicationListenerSynchronization
+ implements org.springframework.transaction.support.TransactionSynchronization {
+
+ public PlatformSynchronization(AE event, TransactionalApplicationListener listener,
+ List callbacks) {
+
+ super(event, listener, callbacks);
+ }
+
+ @Override
+ public void beforeCommit(boolean readOnly) {
+ if (getTransactionPhase() == TransactionPhase.BEFORE_COMMIT) {
+ processEventWithCallbacks();
+ }
+ }
+
+ @Override
+ public void afterCompletion(int status) {
+ TransactionPhase phase = getTransactionPhase();
+ if (phase == TransactionPhase.AFTER_COMMIT && status == STATUS_COMMITTED) {
+ processEventWithCallbacks();
+ }
+ else if (phase == TransactionPhase.AFTER_ROLLBACK && status == STATUS_ROLLED_BACK) {
+ processEventWithCallbacks();
+ }
+ else if (phase == TransactionPhase.AFTER_COMPLETION) {
+ processEventWithCallbacks();
+ }
+ }
+ }
+
+
+ private static class ReactiveSynchronization
+ extends TransactionalApplicationListenerSynchronization
+ implements org.springframework.transaction.reactive.TransactionSynchronization {
+
+ public ReactiveSynchronization(AE event, TransactionalApplicationListener listener,
+ List callbacks) {
+
+ super(event, listener, callbacks);
+ }
+
+ @Override
+ public Mono beforeCommit(boolean readOnly) {
+ if (getTransactionPhase() == TransactionPhase.BEFORE_COMMIT) {
+ return Mono.fromRunnable(this::processEventWithCallbacks);
+ }
+ return Mono.empty();
+ }
+
+ @Override
+ public Mono afterCompletion(int status) {
+ TransactionPhase phase = getTransactionPhase();
+ if (phase == TransactionPhase.AFTER_COMMIT && status == STATUS_COMMITTED) {
+ return Mono.fromRunnable(this::processEventWithCallbacks);
+ }
+ else if (phase == TransactionPhase.AFTER_ROLLBACK && status == STATUS_ROLLED_BACK) {
+ return Mono.fromRunnable(this::processEventWithCallbacks);
+ }
+ else if (phase == TransactionPhase.AFTER_COMPLETION) {
+ return Mono.fromRunnable(this::processEventWithCallbacks);
+ }
+ return Mono.empty();
+ }
+ }
+
}
diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java
index f70022e7cd..3ade90efc8 100644
--- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java
+++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -37,12 +37,13 @@ import org.springframework.core.annotation.AliasFor;
* method allows you to prioritize that listener amongst other listeners running before
* or after transaction completion.
*
- *
NOTE: Transactional event listeners only work with thread-bound transactions
- * managed by a {@link org.springframework.transaction.PlatformTransactionManager
- * PlatformTransactionManager}. A reactive transaction managed by a
- * {@link org.springframework.transaction.ReactiveTransactionManager ReactiveTransactionManager}
- * uses the Reactor context instead of thread-local variables, so from the perspective of
- * an event listener, there is no compatible active transaction that it can participate in.
+ *
As of 6.1, transactional event listeners can work with thread-bound transactions managed
+ * by a {@link org.springframework.transaction.PlatformTransactionManager} as well as reactive
+ * transactions managed by a {@link org.springframework.transaction.ReactiveTransactionManager}.
+ * For the former, listeners are guaranteed to see the current thread-bound transaction.
+ * Since the latter uses the Reactor context instead of thread-local variables, the transaction
+ * context needs to be included in the published event instance as the event source:
+ * see {@link org.springframework.transaction.reactive.TransactionalEventPublisher}.
*
*
WARNING: if the {@code TransactionPhase} is set to
* {@link TransactionPhase#AFTER_COMMIT AFTER_COMMIT} (the default),
diff --git a/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionalEventPublisher.java b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionalEventPublisher.java
new file mode 100644
index 0000000000..9a778e66c3
--- /dev/null
+++ b/spring-tx/src/main/java/org/springframework/transaction/reactive/TransactionalEventPublisher.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2002-2023 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.reactive;
+
+import java.util.function.Function;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.PayloadApplicationEvent;
+
+/**
+ * A delegate for publishing transactional events in a reactive setup.
+ * Includes the current Reactor-managed {@link TransactionContext} as
+ * a source object for every {@link ApplicationEvent} to be published.
+ *
+ *
This delegate is just a convenience. The current {@link TransactionContext}
+ * can be directly included as the event source as well, and then published
+ * through an {@link ApplicationEventPublisher} such as the Spring
+ * {@link org.springframework.context.ApplicationContext}:
+ *
+ *
+ *
+ * @author Juergen Hoeller
+ * @since 6.1
+ * @see #publishEvent(Function)
+ * @see #publishEvent(Object)
+ * @see ApplicationEventPublisher
+ */
+public class TransactionalEventPublisher {
+
+ private final ApplicationEventPublisher eventPublisher;
+
+
+ /**
+ * Create a new delegate for publishing transactional events in a reactive setup.
+ * @param eventPublisher the actual event publisher to use,
+ * typically a Spring {@link org.springframework.context.ApplicationContext}
+ */
+ public TransactionalEventPublisher(ApplicationEventPublisher eventPublisher) {
+ this.eventPublisher = eventPublisher;
+ }
+
+
+ /**
+ * Publish an event created through the given function which maps the transaction
+ * source object (the {@link TransactionContext}) to the event instance.
+ * @param eventCreationFunction a function mapping the source object to the event instance,
+ * e.g. {@code source -> new PayloadApplicationEvent<>(source, "myPayload")}
+ * @return the Reactor {@link Mono} for the transactional event publication
+ */
+ public Mono publishEvent(Function eventCreationFunction) {
+ return TransactionContextManager.currentContext().map(eventCreationFunction)
+ .doOnSuccess(this.eventPublisher::publishEvent).then();
+ }
+
+ /**
+ * Publish an event created for the given payload.
+ * @param payload the payload to publish as an event
+ * @return the Reactor {@link Mono} for the transactional event publication
+ */
+ public Mono publishEvent(Object payload) {
+ if (payload instanceof ApplicationEvent) {
+ return Mono.error(new IllegalArgumentException("Cannot publish ApplicationEvent with transactional " +
+ "source - publish payload object or use publishEvent(Function