Support for transactional listeners with reactive transactions
TransactionalApplicationListener and TransactionalEventListener automatically detect a reactive TransactionContext as the event source and register the synchronization accordingly. TransactionalEventPublisher is a convenient delegate for publishing corresponding events with the current TransactionContext as event source. This can also serve as a guideline for similar reactive event purposes. Closes gh-27515 Closes gh-21025 Closes gh-30244
This commit is contained in:
parent
a9d100eeee
commit
450cc212a2
|
@ -57,10 +57,14 @@ attribute of the annotation to `true`.
|
||||||
|
|
||||||
[NOTE]
|
[NOTE]
|
||||||
====
|
====
|
||||||
`@TransactionalEventListener` only works with thread-bound transactions managed by
|
As of 6.1, `@TransactionalEventListener` can work with thread-bound transactions managed by
|
||||||
`PlatformTransactionManager`. A reactive transaction managed by `ReactiveTransactionManager`
|
`PlatformTransactionManager` as well as reactive transactions managed by `ReactiveTransactionManager`.
|
||||||
uses the Reactor context instead of thread-local attributes, so from the perspective of
|
For the former, listeners are guaranteed to see the current thread-bound transaction.
|
||||||
an event listener, there is no compatible active transaction that it can participate in.
|
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.
|
||||||
====
|
====
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.ApplicationEvent
|
||||||
* @see org.springframework.context.event.ApplicationEventMulticaster
|
* @see org.springframework.context.event.ApplicationEventMulticaster
|
||||||
* @see org.springframework.context.event.EventPublicationInterceptor
|
* @see org.springframework.context.event.EventPublicationInterceptor
|
||||||
|
* @see org.springframework.transaction.event.TransactionalApplicationListener
|
||||||
*/
|
*/
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface ApplicationEventPublisher {
|
public interface ApplicationEventPublisher {
|
||||||
|
@ -42,8 +43,21 @@ public interface ApplicationEventPublisher {
|
||||||
* or even immediate execution at all. Event listeners are encouraged
|
* or even immediate execution at all. Event listeners are encouraged
|
||||||
* to be as efficient as possible, individually using asynchronous
|
* to be as efficient as possible, individually using asynchronous
|
||||||
* execution for longer-running and potentially blocking operations.
|
* execution for longer-running and potentially blocking operations.
|
||||||
|
* <p>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.
|
||||||
|
* <p>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
|
* @param event the event to publish
|
||||||
* @see #publishEvent(Object)
|
* @see #publishEvent(Object)
|
||||||
|
* @see ApplicationListener#supportsAsyncExecution()
|
||||||
* @see org.springframework.context.event.ContextRefreshedEvent
|
* @see org.springframework.context.event.ContextRefreshedEvent
|
||||||
* @see org.springframework.context.event.ContextClosedEvent
|
* @see org.springframework.context.event.ContextClosedEvent
|
||||||
*/
|
*/
|
||||||
|
@ -61,6 +75,11 @@ public interface ApplicationEventPublisher {
|
||||||
* or even immediate execution at all. Event listeners are encouraged
|
* or even immediate execution at all. Event listeners are encouraged
|
||||||
* to be as efficient as possible, individually using asynchronous
|
* to be as efficient as possible, individually using asynchronous
|
||||||
* execution for longer-running and potentially blocking operations.
|
* execution for longer-running and potentially blocking operations.
|
||||||
|
* <p>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
|
* @param event the event to publish
|
||||||
* @since 4.2
|
* @since 4.2
|
||||||
* @see #publishEvent(ApplicationEvent)
|
* @see #publishEvent(ApplicationEvent)
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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
|
* allows you to prioritize that listener amongst other listeners running before or after
|
||||||
* transaction completion.
|
* transaction completion.
|
||||||
*
|
*
|
||||||
* <p><b>NOTE: Transactional event listeners only work with thread-bound transactions
|
* <p>As of 6.1, transactional event listeners can work with thread-bound transactions managed
|
||||||
* managed by a {@link org.springframework.transaction.PlatformTransactionManager
|
* by a {@link org.springframework.transaction.PlatformTransactionManager} as well as reactive
|
||||||
* PlatformTransactionManager}.</b> A reactive transaction managed by a
|
* transactions managed by a {@link org.springframework.transaction.ReactiveTransactionManager}.
|
||||||
* {@link org.springframework.transaction.ReactiveTransactionManager ReactiveTransactionManager}
|
* For the former, listeners are guaranteed to see the current thread-bound transaction.
|
||||||
* uses the Reactor context instead of thread-local variables, so from the perspective of
|
* Since the latter uses the Reactor context instead of thread-local variables, the transaction
|
||||||
* an event listener, there is no compatible active transaction that it can participate in.
|
* 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 Juergen Hoeller
|
||||||
* @author Oliver Drotbohm
|
* @author Oliver Drotbohm
|
||||||
|
@ -60,6 +61,16 @@ public interface TransactionalApplicationListener<E extends ApplicationEvent>
|
||||||
return Ordered.LOWEST_PRECEDENCE;
|
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.
|
* Return an identifier for the listener to be able to refer to it individually.
|
||||||
* <p>It might be necessary for specific completion callback implementations
|
* <p>It might be necessary for specific completion callback implementations
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.ApplicationEvent;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -128,11 +127,7 @@ public class TransactionalApplicationListenerAdapter<E extends ApplicationEvent>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onApplicationEvent(E event) {
|
public void onApplicationEvent(E event) {
|
||||||
if (TransactionSynchronizationManager.isSynchronizationActive() &&
|
TransactionalApplicationListenerSynchronization.register(event, this, this.callbacks);
|
||||||
TransactionSynchronizationManager.isActualTransactionActive()) {
|
|
||||||
TransactionSynchronizationManager.registerSynchronization(
|
|
||||||
new TransactionalApplicationListenerSynchronization<>(event, this, this.callbacks));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@ import org.springframework.context.event.ApplicationListenerMethodAdapter;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.context.event.GenericApplicationListener;
|
import org.springframework.context.event.GenericApplicationListener;
|
||||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -87,10 +86,10 @@ public class TransactionalApplicationListenerMethodAdapter extends ApplicationLi
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onApplicationEvent(ApplicationEvent event) {
|
public void onApplicationEvent(ApplicationEvent event) {
|
||||||
if (TransactionSynchronizationManager.isSynchronizationActive() &&
|
if (TransactionalApplicationListenerSynchronization.register(event, this, this.callbacks)) {
|
||||||
TransactionSynchronizationManager.isActualTransactionActive()) {
|
if (logger.isDebugEnabled()) {
|
||||||
TransactionSynchronizationManager.registerSynchronization(
|
logger.debug("Registered transaction synchronization for " + event);
|
||||||
new TransactionalApplicationListenerSynchronization<>(event, this, this.callbacks));
|
}
|
||||||
}
|
}
|
||||||
else if (this.annotation.fallbackExecution()) {
|
else if (this.annotation.fallbackExecution()) {
|
||||||
if (this.annotation.phase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) {
|
if (this.annotation.phase() == TransactionPhase.AFTER_ROLLBACK && logger.isWarnEnabled()) {
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 java.util.List;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.context.ApplicationEvent;
|
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}.
|
* {@link TransactionalApplicationListener}.
|
||||||
*
|
*
|
||||||
* @author Juergen Hoeller
|
* @author Juergen Hoeller
|
||||||
* @since 5.3
|
* @since 5.3
|
||||||
* @param <E> the specific {@code ApplicationEvent} subclass to listen to
|
* @param <E> the specific {@code ApplicationEvent} subclass to listen to
|
||||||
*/
|
*/
|
||||||
class TransactionalApplicationListenerSynchronization<E extends ApplicationEvent>
|
abstract class TransactionalApplicationListenerSynchronization<E extends ApplicationEvent> implements Ordered {
|
||||||
implements TransactionSynchronization {
|
|
||||||
|
|
||||||
private final E event;
|
private final E event;
|
||||||
|
|
||||||
|
@ -53,28 +55,11 @@ class TransactionalApplicationListenerSynchronization<E extends ApplicationEvent
|
||||||
return this.listener.getOrder();
|
return this.listener.getOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public TransactionPhase getTransactionPhase() {
|
||||||
public void beforeCommit(boolean readOnly) {
|
return this.listener.getTransactionPhase();
|
||||||
if (this.listener.getTransactionPhase() == TransactionPhase.BEFORE_COMMIT) {
|
|
||||||
processEventWithCallbacks();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void processEventWithCallbacks() {
|
||||||
public void afterCompletion(int status) {
|
|
||||||
TransactionPhase phase = this.listener.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 void processEventWithCallbacks() {
|
|
||||||
this.callbacks.forEach(callback -> callback.preProcessEvent(this.event));
|
this.callbacks.forEach(callback -> callback.preProcessEvent(this.event));
|
||||||
try {
|
try {
|
||||||
this.listener.processEvent(this.event);
|
this.listener.processEvent(this.event);
|
||||||
|
@ -86,4 +71,94 @@ class TransactionalApplicationListenerSynchronization<E extends ApplicationEvent
|
||||||
this.callbacks.forEach(callback -> callback.postProcessEvent(this.event, null));
|
this.callbacks.forEach(callback -> callback.postProcessEvent(this.event, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static <E extends ApplicationEvent> boolean register(
|
||||||
|
E event, TransactionalApplicationListener<E> listener,
|
||||||
|
List<TransactionalApplicationListener.SynchronizationCallback> 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<AE extends ApplicationEvent>
|
||||||
|
extends TransactionalApplicationListenerSynchronization<AE>
|
||||||
|
implements org.springframework.transaction.support.TransactionSynchronization {
|
||||||
|
|
||||||
|
public PlatformSynchronization(AE event, TransactionalApplicationListener<AE> listener,
|
||||||
|
List<TransactionalApplicationListener.SynchronizationCallback> 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<AE extends ApplicationEvent>
|
||||||
|
extends TransactionalApplicationListenerSynchronization<AE>
|
||||||
|
implements org.springframework.transaction.reactive.TransactionSynchronization {
|
||||||
|
|
||||||
|
public ReactiveSynchronization(AE event, TransactionalApplicationListener<AE> listener,
|
||||||
|
List<TransactionalApplicationListener.SynchronizationCallback> callbacks) {
|
||||||
|
|
||||||
|
super(event, listener, callbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> beforeCommit(boolean readOnly) {
|
||||||
|
if (getTransactionPhase() == TransactionPhase.BEFORE_COMMIT) {
|
||||||
|
return Mono.fromRunnable(this::processEventWithCallbacks);
|
||||||
|
}
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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
|
* method allows you to prioritize that listener amongst other listeners running before
|
||||||
* or after transaction completion.
|
* or after transaction completion.
|
||||||
*
|
*
|
||||||
* <p><b>NOTE: Transactional event listeners only work with thread-bound transactions
|
* <p>As of 6.1, transactional event listeners can work with thread-bound transactions managed
|
||||||
* managed by a {@link org.springframework.transaction.PlatformTransactionManager
|
* by a {@link org.springframework.transaction.PlatformTransactionManager} as well as reactive
|
||||||
* PlatformTransactionManager}.</b> A reactive transaction managed by a
|
* transactions managed by a {@link org.springframework.transaction.ReactiveTransactionManager}.
|
||||||
* {@link org.springframework.transaction.ReactiveTransactionManager ReactiveTransactionManager}
|
* For the former, listeners are guaranteed to see the current thread-bound transaction.
|
||||||
* uses the Reactor context instead of thread-local variables, so from the perspective of
|
* Since the latter uses the Reactor context instead of thread-local variables, the transaction
|
||||||
* an event listener, there is no compatible active transaction that it can participate in.
|
* context needs to be included in the published event instance as the event source:
|
||||||
|
* see {@link org.springframework.transaction.reactive.TransactionalEventPublisher}.
|
||||||
*
|
*
|
||||||
* <p><strong>WARNING:</strong> if the {@code TransactionPhase} is set to
|
* <p><strong>WARNING:</strong> if the {@code TransactionPhase} is set to
|
||||||
* {@link TransactionPhase#AFTER_COMMIT AFTER_COMMIT} (the default),
|
* {@link TransactionPhase#AFTER_COMMIT AFTER_COMMIT} (the default),
|
||||||
|
|
|
@ -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.
|
||||||
|
*
|
||||||
|
* <p>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}:
|
||||||
|
*
|
||||||
|
* <pre class="code">
|
||||||
|
* TransactionContextManager.currentContext()
|
||||||
|
* .map(source -> new PayloadApplicationEvent<>(source, "myPayload"))
|
||||||
|
* .doOnSuccess(this.eventPublisher::publishEvent)
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @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<Void> publishEvent(Function<TransactionContext, ApplicationEvent> 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<Void> 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<Object, ApplicationEvent>"));
|
||||||
|
}
|
||||||
|
return publishEvent(source -> new PayloadApplicationEvent<>(source, payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,508 @@
|
||||||
|
/*
|
||||||
|
* 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.event;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.transaction.reactive.TransactionalEventPublisher;
|
||||||
|
import org.springframework.transaction.reactive.TransactionalOperator;
|
||||||
|
import org.springframework.transaction.support.TransactionSynchronization;
|
||||||
|
import org.springframework.transaction.testfixture.ReactiveCallCountingTransactionManager;
|
||||||
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
|
import org.springframework.util.MultiValueMap;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
import static org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT;
|
||||||
|
import static org.springframework.transaction.event.TransactionPhase.AFTER_COMPLETION;
|
||||||
|
import static org.springframework.transaction.event.TransactionPhase.AFTER_ROLLBACK;
|
||||||
|
import static org.springframework.transaction.event.TransactionPhase.BEFORE_COMMIT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for {@link TransactionalEventListener} support
|
||||||
|
* with reactive transactions.
|
||||||
|
*
|
||||||
|
* @author Juergen Hoeller
|
||||||
|
* @since 6.1
|
||||||
|
*/
|
||||||
|
public class ReactiveTransactionalEventListenerTests {
|
||||||
|
|
||||||
|
private ConfigurableApplicationContext context;
|
||||||
|
|
||||||
|
private EventCollector eventCollector;
|
||||||
|
|
||||||
|
private TransactionalOperator transactionalOperator;
|
||||||
|
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
public void closeContext() {
|
||||||
|
if (this.context != null) {
|
||||||
|
this.context.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void immediately() {
|
||||||
|
load(ImmediateTestListener.class);
|
||||||
|
this.transactionalOperator.execute(status -> publishEvent("test").then(Mono.fromRunnable(() -> {
|
||||||
|
getEventCollector().assertEvents(EventCollector.IMMEDIATELY, "test");
|
||||||
|
getEventCollector().assertTotalEventsCount(1);
|
||||||
|
}))).blockFirst();
|
||||||
|
getEventCollector().assertEvents(EventCollector.IMMEDIATELY, "test");
|
||||||
|
getEventCollector().assertTotalEventsCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void immediatelyImpactsCurrentTransaction() {
|
||||||
|
load(ImmediateTestListener.class, BeforeCommitTestListener.class);
|
||||||
|
assertThatIllegalStateException().isThrownBy(() ->
|
||||||
|
this.transactionalOperator.execute(status -> publishEvent("FAIL").then(Mono.fromRunnable(() -> {
|
||||||
|
throw new AssertionError("Should have thrown an exception at this point");
|
||||||
|
}))).blockFirst())
|
||||||
|
.withMessageContaining("Test exception")
|
||||||
|
.withMessageContaining(EventCollector.IMMEDIATELY);
|
||||||
|
|
||||||
|
getEventCollector().assertEvents(EventCollector.IMMEDIATELY, "FAIL");
|
||||||
|
getEventCollector().assertTotalEventsCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void afterCompletionCommit() {
|
||||||
|
load(AfterCompletionTestListener.class);
|
||||||
|
this.transactionalOperator.execute(status -> publishEvent("test")
|
||||||
|
.then(Mono.fromRunnable(() -> getEventCollector().assertNoEventReceived()))).blockFirst();
|
||||||
|
getEventCollector().assertEvents(EventCollector.AFTER_COMPLETION, "test");
|
||||||
|
getEventCollector().assertTotalEventsCount(1); // After rollback not invoked
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void afterCompletionRollback() {
|
||||||
|
load(AfterCompletionTestListener.class);
|
||||||
|
this.transactionalOperator.execute(status -> publishEvent("test").then(Mono.fromRunnable(() -> {
|
||||||
|
getEventCollector().assertNoEventReceived();
|
||||||
|
status.setRollbackOnly();
|
||||||
|
}))).blockFirst();
|
||||||
|
getEventCollector().assertEvents(EventCollector.AFTER_COMPLETION, "test");
|
||||||
|
getEventCollector().assertTotalEventsCount(1); // After rollback not invoked
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void afterCommit() {
|
||||||
|
load(AfterCompletionExplicitTestListener.class);
|
||||||
|
this.transactionalOperator.execute(status -> publishEvent("test")
|
||||||
|
.then(Mono.fromRunnable(() -> getEventCollector().assertNoEventReceived()))).blockFirst();
|
||||||
|
getEventCollector().assertEvents(EventCollector.AFTER_COMMIT, "test");
|
||||||
|
getEventCollector().assertTotalEventsCount(1); // After rollback not invoked
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void afterCommitWithTransactionalComponentListenerProxiedViaDynamicProxy() {
|
||||||
|
load(TransactionalComponentTestListener.class);
|
||||||
|
this.transactionalOperator.execute(status -> publishEvent("SKIP")
|
||||||
|
.then(Mono.fromRunnable(() -> getEventCollector().assertNoEventReceived()))).blockFirst();
|
||||||
|
getEventCollector().assertNoEventReceived();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void afterRollback() {
|
||||||
|
load(AfterCompletionExplicitTestListener.class);
|
||||||
|
this.transactionalOperator.execute(status -> publishEvent("test").then(Mono.fromRunnable(() -> {
|
||||||
|
getEventCollector().assertNoEventReceived();
|
||||||
|
status.setRollbackOnly();
|
||||||
|
}))).blockFirst();
|
||||||
|
getEventCollector().assertEvents(EventCollector.AFTER_ROLLBACK, "test");
|
||||||
|
getEventCollector().assertTotalEventsCount(1); // After commit not invoked
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void beforeCommit() {
|
||||||
|
load(BeforeCommitTestListener.class);
|
||||||
|
this.transactionalOperator.execute(status -> publishEvent("test")
|
||||||
|
.then(Mono.fromRunnable(() -> getEventCollector().assertNoEventReceived()))).blockFirst();
|
||||||
|
getEventCollector().assertEvents(EventCollector.BEFORE_COMMIT, "test");
|
||||||
|
getEventCollector().assertTotalEventsCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noTransaction() {
|
||||||
|
load(BeforeCommitTestListener.class, AfterCompletionTestListener.class,
|
||||||
|
AfterCompletionExplicitTestListener.class);
|
||||||
|
publishEvent("test");
|
||||||
|
getEventCollector().assertTotalEventsCount(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void transactionDemarcationWithNotSupportedPropagation() {
|
||||||
|
load(BeforeCommitTestListener.class, AfterCompletionTestListener.class);
|
||||||
|
getContext().getBean(TestBean.class).notSupported().block();
|
||||||
|
getEventCollector().assertTotalEventsCount(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void transactionDemarcationWithSupportsPropagationAndNoTransaction() {
|
||||||
|
load(BeforeCommitTestListener.class, AfterCompletionTestListener.class);
|
||||||
|
getContext().getBean(TestBean.class).supports().block();
|
||||||
|
getEventCollector().assertTotalEventsCount(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void transactionDemarcationWithSupportsPropagationAndExistingTransaction() {
|
||||||
|
load(BeforeCommitTestListener.class, AfterCompletionTestListener.class);
|
||||||
|
this.transactionalOperator.execute(status -> getContext().getBean(TestBean.class).supports()
|
||||||
|
.then(Mono.fromRunnable(() -> getEventCollector().assertNoEventReceived()))).blockFirst();
|
||||||
|
getEventCollector().assertTotalEventsCount(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void transactionDemarcationWithRequiredPropagation() {
|
||||||
|
load(BeforeCommitTestListener.class, AfterCompletionTestListener.class);
|
||||||
|
getContext().getBean(TestBean.class).required().block();
|
||||||
|
getEventCollector().assertTotalEventsCount(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void noTransactionWithFallbackExecution() {
|
||||||
|
load(FallbackExecutionTestListener.class);
|
||||||
|
getContext().publishEvent("test");
|
||||||
|
this.eventCollector.assertEvents(EventCollector.BEFORE_COMMIT, "test");
|
||||||
|
this.eventCollector.assertEvents(EventCollector.AFTER_COMMIT, "test");
|
||||||
|
this.eventCollector.assertEvents(EventCollector.AFTER_ROLLBACK, "test");
|
||||||
|
this.eventCollector.assertEvents(EventCollector.AFTER_COMPLETION, "test");
|
||||||
|
getEventCollector().assertTotalEventsCount(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void conditionFoundOnTransactionalEventListener() {
|
||||||
|
load(ImmediateTestListener.class);
|
||||||
|
this.transactionalOperator.execute(status -> publishEvent("SKIP")
|
||||||
|
.then(Mono.fromRunnable(() -> getEventCollector().assertNoEventReceived()))).blockFirst();
|
||||||
|
getEventCollector().assertNoEventReceived();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void afterCommitMetaAnnotation() {
|
||||||
|
load(AfterCommitMetaAnnotationTestListener.class);
|
||||||
|
this.transactionalOperator.execute(status -> publishEvent("test")
|
||||||
|
.then(Mono.fromRunnable(() -> getEventCollector().assertNoEventReceived()))).blockFirst();
|
||||||
|
getEventCollector().assertEvents(EventCollector.AFTER_COMMIT, "test");
|
||||||
|
getEventCollector().assertTotalEventsCount(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void conditionFoundOnMetaAnnotation() {
|
||||||
|
load(AfterCommitMetaAnnotationTestListener.class);
|
||||||
|
this.transactionalOperator.execute(status -> publishEvent("SKIP")
|
||||||
|
.then(Mono.fromRunnable(() -> getEventCollector().assertNoEventReceived()))).blockFirst();
|
||||||
|
getEventCollector().assertNoEventReceived();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected EventCollector getEventCollector() {
|
||||||
|
return this.eventCollector;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ConfigurableApplicationContext getContext() {
|
||||||
|
return this.context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load(Class<?>... classes) {
|
||||||
|
List<Class<?>> allClasses = new ArrayList<>();
|
||||||
|
allClasses.add(BasicConfiguration.class);
|
||||||
|
allClasses.addAll(Arrays.asList(classes));
|
||||||
|
doLoad(allClasses.toArray(new Class<?>[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doLoad(Class<?>... classes) {
|
||||||
|
this.context = new AnnotationConfigApplicationContext(classes);
|
||||||
|
this.eventCollector = this.context.getBean(EventCollector.class);
|
||||||
|
this.transactionalOperator = this.context.getBean(TransactionalOperator.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Void> publishEvent(Object event) {
|
||||||
|
return new TransactionalEventPublisher(getContext()).publishEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableTransactionManagement
|
||||||
|
static class BasicConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public EventCollector eventCollector() {
|
||||||
|
return new EventCollector();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public TestBean testBean(ApplicationEventPublisher eventPublisher) {
|
||||||
|
return new TestBean(eventPublisher);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ReactiveCallCountingTransactionManager transactionManager() {
|
||||||
|
return new ReactiveCallCountingTransactionManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public TransactionalOperator transactionTemplate() {
|
||||||
|
return TransactionalOperator.create(transactionManager());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class EventCollector {
|
||||||
|
|
||||||
|
public static final String IMMEDIATELY = "IMMEDIATELY";
|
||||||
|
|
||||||
|
public static final String BEFORE_COMMIT = "BEFORE_COMMIT";
|
||||||
|
|
||||||
|
public static final String AFTER_COMPLETION = "AFTER_COMPLETION";
|
||||||
|
|
||||||
|
public static final String AFTER_COMMIT = "AFTER_COMMIT";
|
||||||
|
|
||||||
|
public static final String AFTER_ROLLBACK = "AFTER_ROLLBACK";
|
||||||
|
|
||||||
|
public static final String[] ALL_PHASES = {IMMEDIATELY, BEFORE_COMMIT, AFTER_COMMIT, AFTER_ROLLBACK};
|
||||||
|
|
||||||
|
private final MultiValueMap<String, Object> events = new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
|
public void addEvent(String phase, Object event) {
|
||||||
|
this.events.add(phase, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Object> getEvents(String phase) {
|
||||||
|
return this.events.getOrDefault(phase, Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertNoEventReceived(String... phases) {
|
||||||
|
if (phases.length == 0) { // All values if none set
|
||||||
|
phases = ALL_PHASES;
|
||||||
|
}
|
||||||
|
for (String phase : phases) {
|
||||||
|
List<Object> eventsForPhase = getEvents(phase);
|
||||||
|
assertThat(eventsForPhase.size()).as("Expected no events for phase '" + phase + "' " +
|
||||||
|
"but got " + eventsForPhase + ":").isEqualTo(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertEvents(String phase, Object... expected) {
|
||||||
|
List<Object> actual = getEvents(phase);
|
||||||
|
assertThat(actual.size()).as("wrong number of events for phase '" + phase + "'").isEqualTo(expected.length);
|
||||||
|
for (int i = 0; i < expected.length; i++) {
|
||||||
|
assertThat(actual.get(i)).as("Wrong event for phase '" + phase + "' at index " + i).isEqualTo(expected[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void assertTotalEventsCount(int number) {
|
||||||
|
int size = 0;
|
||||||
|
for (Map.Entry<String, List<Object>> entry : this.events.entrySet()) {
|
||||||
|
size += entry.getValue().size();
|
||||||
|
}
|
||||||
|
assertThat(size).as("Wrong number of total events (" + this.events.size() + ") " +
|
||||||
|
"registered phase(s)").isEqualTo(number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class TestBean {
|
||||||
|
|
||||||
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
TestBean(ApplicationEventPublisher eventPublisher) {
|
||||||
|
this.eventPublisher = eventPublisher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||||
|
public Mono<Void> notSupported() {
|
||||||
|
return new TransactionalEventPublisher(this.eventPublisher).publishEvent("test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(propagation = Propagation.SUPPORTS)
|
||||||
|
public Mono<Void> supports() {
|
||||||
|
return new TransactionalEventPublisher(this.eventPublisher).publishEvent("test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(propagation = Propagation.REQUIRED)
|
||||||
|
public Mono<Void> required() {
|
||||||
|
return new TransactionalEventPublisher(this.eventPublisher).publishEvent("test");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static abstract class BaseTransactionalTestListener {
|
||||||
|
|
||||||
|
static final String FAIL_MSG = "FAIL";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EventCollector eventCollector;
|
||||||
|
|
||||||
|
public void handleEvent(String phase, String data) {
|
||||||
|
this.eventCollector.addEvent(phase, data);
|
||||||
|
if (FAIL_MSG.equals(data)) {
|
||||||
|
throw new IllegalStateException("Test exception on phase '" + phase + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component
|
||||||
|
static class ImmediateTestListener extends BaseTransactionalTestListener {
|
||||||
|
|
||||||
|
@EventListener(condition = "!'SKIP'.equals(#data)")
|
||||||
|
public void handleImmediately(String data) {
|
||||||
|
handleEvent(EventCollector.IMMEDIATELY, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component
|
||||||
|
static class AfterCompletionTestListener extends BaseTransactionalTestListener {
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = AFTER_COMPLETION)
|
||||||
|
public void handleAfterCompletion(String data) {
|
||||||
|
handleEvent(EventCollector.AFTER_COMPLETION, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component
|
||||||
|
static class AfterCompletionExplicitTestListener extends BaseTransactionalTestListener {
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = AFTER_COMMIT)
|
||||||
|
public void handleAfterCommit(String data) {
|
||||||
|
handleEvent(EventCollector.AFTER_COMMIT, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = AFTER_ROLLBACK)
|
||||||
|
public void handleAfterRollback(String data) {
|
||||||
|
handleEvent(EventCollector.AFTER_ROLLBACK, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Component
|
||||||
|
interface TransactionalComponentTestListenerInterface {
|
||||||
|
|
||||||
|
// Cannot use #data in condition due to dynamic proxy.
|
||||||
|
@TransactionalEventListener(condition = "!'SKIP'.equals(#p0)")
|
||||||
|
void handleAfterCommit(String data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class TransactionalComponentTestListener extends BaseTransactionalTestListener implements
|
||||||
|
TransactionalComponentTestListenerInterface {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleAfterCommit(String data) {
|
||||||
|
handleEvent(EventCollector.AFTER_COMMIT, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component
|
||||||
|
static class BeforeCommitTestListener extends BaseTransactionalTestListener {
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = BEFORE_COMMIT)
|
||||||
|
@Order(15)
|
||||||
|
public void handleBeforeCommit(String data) {
|
||||||
|
handleEvent(EventCollector.BEFORE_COMMIT, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component
|
||||||
|
static class FallbackExecutionTestListener extends BaseTransactionalTestListener {
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = BEFORE_COMMIT, fallbackExecution = true)
|
||||||
|
public void handleBeforeCommit(String data) {
|
||||||
|
handleEvent(EventCollector.BEFORE_COMMIT, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = AFTER_COMMIT, fallbackExecution = true)
|
||||||
|
public void handleAfterCommit(String data) {
|
||||||
|
handleEvent(EventCollector.AFTER_COMMIT, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = AFTER_ROLLBACK, fallbackExecution = true)
|
||||||
|
public void handleAfterRollback(String data) {
|
||||||
|
handleEvent(EventCollector.AFTER_ROLLBACK, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = AFTER_COMPLETION, fallbackExecution = true)
|
||||||
|
public void handleAfterCompletion(String data) {
|
||||||
|
handleEvent(EventCollector.AFTER_COMPLETION, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@TransactionalEventListener(phase = AFTER_COMMIT, condition = "!'SKIP'.equals(#p0)")
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@interface AfterCommitEventListener {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component
|
||||||
|
static class AfterCommitMetaAnnotationTestListener extends BaseTransactionalTestListener {
|
||||||
|
|
||||||
|
@AfterCommitEventListener
|
||||||
|
public void handleAfterCommit(String data) {
|
||||||
|
handleEvent(EventCollector.AFTER_COMMIT, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static class EventTransactionSynchronization implements TransactionSynchronization {
|
||||||
|
|
||||||
|
private final int order;
|
||||||
|
|
||||||
|
EventTransactionSynchronization(int order) {
|
||||||
|
this.order = order;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -36,7 +36,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.context.event.SimpleApplicationEventMulticaster;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
|
@ -57,6 +59,7 @@ import static org.springframework.transaction.event.TransactionPhase.BEFORE_COMM
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integration tests for {@link TransactionalEventListener} support
|
* Integration tests for {@link TransactionalEventListener} support
|
||||||
|
* with thread-bound transactions.
|
||||||
*
|
*
|
||||||
* @author Stephane Nicoll
|
* @author Stephane Nicoll
|
||||||
* @author Sam Brannen
|
* @author Sam Brannen
|
||||||
|
@ -87,7 +90,6 @@ public class TransactionalEventListenerTests {
|
||||||
getEventCollector().assertEvents(EventCollector.IMMEDIATELY, "test");
|
getEventCollector().assertEvents(EventCollector.IMMEDIATELY, "test");
|
||||||
getEventCollector().assertTotalEventsCount(1);
|
getEventCollector().assertTotalEventsCount(1);
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
});
|
});
|
||||||
getEventCollector().assertEvents(EventCollector.IMMEDIATELY, "test");
|
getEventCollector().assertEvents(EventCollector.IMMEDIATELY, "test");
|
||||||
getEventCollector().assertTotalEventsCount(1);
|
getEventCollector().assertTotalEventsCount(1);
|
||||||
|
@ -115,7 +117,6 @@ public class TransactionalEventListenerTests {
|
||||||
getContext().publishEvent("test");
|
getContext().publishEvent("test");
|
||||||
getEventCollector().assertNoEventReceived();
|
getEventCollector().assertNoEventReceived();
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
});
|
});
|
||||||
getEventCollector().assertEvents(EventCollector.AFTER_COMPLETION, "test");
|
getEventCollector().assertEvents(EventCollector.AFTER_COMPLETION, "test");
|
||||||
getEventCollector().assertTotalEventsCount(1); // After rollback not invoked
|
getEventCollector().assertTotalEventsCount(1); // After rollback not invoked
|
||||||
|
@ -129,7 +130,6 @@ public class TransactionalEventListenerTests {
|
||||||
getEventCollector().assertNoEventReceived();
|
getEventCollector().assertNoEventReceived();
|
||||||
status.setRollbackOnly();
|
status.setRollbackOnly();
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
});
|
});
|
||||||
getEventCollector().assertEvents(EventCollector.AFTER_COMPLETION, "test");
|
getEventCollector().assertEvents(EventCollector.AFTER_COMPLETION, "test");
|
||||||
getEventCollector().assertTotalEventsCount(1); // After rollback not invoked
|
getEventCollector().assertTotalEventsCount(1); // After rollback not invoked
|
||||||
|
@ -142,7 +142,6 @@ public class TransactionalEventListenerTests {
|
||||||
getContext().publishEvent("test");
|
getContext().publishEvent("test");
|
||||||
getEventCollector().assertNoEventReceived();
|
getEventCollector().assertNoEventReceived();
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
});
|
});
|
||||||
getEventCollector().assertEvents(EventCollector.AFTER_COMMIT, "test");
|
getEventCollector().assertEvents(EventCollector.AFTER_COMMIT, "test");
|
||||||
getEventCollector().assertTotalEventsCount(1); // After rollback not invoked
|
getEventCollector().assertTotalEventsCount(1); // After rollback not invoked
|
||||||
|
@ -172,6 +171,19 @@ public class TransactionalEventListenerTests {
|
||||||
getEventCollector().assertTotalEventsCount(1); // After commit not invoked
|
getEventCollector().assertTotalEventsCount(1); // After commit not invoked
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void afterRollbackWithCustomExecutor() {
|
||||||
|
load(AfterCompletionExplicitTestListener.class, MulticasterWithCustomExecutor.class);
|
||||||
|
this.transactionTemplate.execute(status -> {
|
||||||
|
getContext().publishEvent("test");
|
||||||
|
getEventCollector().assertNoEventReceived();
|
||||||
|
status.setRollbackOnly();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
getEventCollector().assertEvents(EventCollector.AFTER_ROLLBACK, "test");
|
||||||
|
getEventCollector().assertTotalEventsCount(1); // After commit not invoked
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void beforeCommit() {
|
public void beforeCommit() {
|
||||||
load(BeforeCommitTestListener.class);
|
load(BeforeCommitTestListener.class);
|
||||||
|
@ -307,13 +319,12 @@ public class TransactionalEventListenerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void afterCommitMetaAnnotation() throws Exception {
|
public void afterCommitMetaAnnotation() {
|
||||||
load(AfterCommitMetaAnnotationTestListener.class);
|
load(AfterCommitMetaAnnotationTestListener.class);
|
||||||
this.transactionTemplate.execute(status -> {
|
this.transactionTemplate.execute(status -> {
|
||||||
getContext().publishEvent("test");
|
getContext().publishEvent("test");
|
||||||
getEventCollector().assertNoEventReceived();
|
getEventCollector().assertNoEventReceived();
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
});
|
});
|
||||||
getEventCollector().assertEvents(EventCollector.AFTER_COMMIT, "test");
|
getEventCollector().assertEvents(EventCollector.AFTER_COMMIT, "test");
|
||||||
getEventCollector().assertTotalEventsCount(1);
|
getEventCollector().assertTotalEventsCount(1);
|
||||||
|
@ -326,7 +337,6 @@ public class TransactionalEventListenerTests {
|
||||||
getContext().publishEvent("SKIP");
|
getContext().publishEvent("SKIP");
|
||||||
getEventCollector().assertNoEventReceived();
|
getEventCollector().assertNoEventReceived();
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
});
|
});
|
||||||
getEventCollector().assertNoEventReceived();
|
getEventCollector().assertNoEventReceived();
|
||||||
}
|
}
|
||||||
|
@ -380,6 +390,18 @@ public class TransactionalEventListenerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
static class MulticasterWithCustomExecutor {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SimpleApplicationEventMulticaster applicationEventMulticaster() {
|
||||||
|
SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
|
||||||
|
multicaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
|
||||||
|
return multicaster;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static class EventCollector {
|
static class EventCollector {
|
||||||
|
|
||||||
public static final String IMMEDIATELY = "IMMEDIATELY";
|
public static final String IMMEDIATELY = "IMMEDIATELY";
|
||||||
|
|
Loading…
Reference in New Issue