Browse Source
Includes forPayload methods and common adapter classes for programmatic usage. Aligns default order values for event handling delegates to LOWEST_PRECEDENCE. Closes gh-24163pull/23650/merge
19 changed files with 905 additions and 184 deletions
@ -0,0 +1,155 @@
@@ -0,0 +1,155 @@
|
||||
/* |
||||
* Copyright 2002-2020 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.util.function.Consumer; |
||||
|
||||
import org.springframework.context.ApplicationEvent; |
||||
import org.springframework.context.ApplicationListener; |
||||
import org.springframework.context.PayloadApplicationEvent; |
||||
import org.springframework.core.Ordered; |
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* An {@link ApplicationListener} that is invoked according to a {@link TransactionPhase}. |
||||
* This is a programmatic equivalent of the {@link TransactionalEventListener} annotation. |
||||
* |
||||
* <p>Adding {@link org.springframework.core.Ordered} to your listener implementation |
||||
* allows you to prioritize that listener amongst other listeners running before or after |
||||
* transaction completion. |
||||
* |
||||
* <p><b>NOTE: Transactional event listeners only work with thread-bound transactions |
||||
* managed by {@link org.springframework.transaction.PlatformTransactionManager}.</b> |
||||
* A reactive transaction managed by {@link org.springframework.transaction.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. |
||||
* |
||||
* @author Juergen Hoeller |
||||
* @author Oliver Drotbohm |
||||
* @since 5.3 |
||||
* @param <E> the specific {@code ApplicationEvent} subclass to listen to |
||||
* @see TransactionalEventListener |
||||
* @see TransactionalApplicationListenerAdapter |
||||
* @see #forPayload |
||||
*/ |
||||
public interface TransactionalApplicationListener<E extends ApplicationEvent> |
||||
extends ApplicationListener<E>, Ordered { |
||||
|
||||
/** |
||||
* Return the execution order within transaction synchronizations. |
||||
* <p>Default is {@link Ordered#LOWEST_PRECEDENCE}. |
||||
* @see org.springframework.transaction.support.TransactionSynchronization#getOrder() |
||||
*/ |
||||
@Override |
||||
default int getOrder() { |
||||
return Ordered.LOWEST_PRECEDENCE; |
||||
} |
||||
|
||||
/** |
||||
* Return the {@link TransactionPhase} in which the listener will be invoked. |
||||
* <p>The default phase is {@link TransactionPhase#AFTER_COMMIT}. |
||||
*/ |
||||
default TransactionPhase getTransactionPhase() { |
||||
return TransactionPhase.AFTER_COMMIT; |
||||
} |
||||
|
||||
/** |
||||
* Return an identifier for the listener to be able to refer to it individually. |
||||
* <p>It might be necessary for specific completion callback implementations |
||||
* to provide a specific id, whereas for other scenarios an empty String |
||||
* (as the common default value) is acceptable as well. |
||||
* @see #addCallback |
||||
*/ |
||||
default String getListenerId() { |
||||
return ""; |
||||
} |
||||
|
||||
/** |
||||
* Add a callback to be invoked on processing within transaction synchronization, |
||||
* i.e. when {@link #processEvent} is being triggered during actual transactions. |
||||
* @param callback the synchronization callback to apply |
||||
*/ |
||||
void addCallback(SynchronizationCallback callback); |
||||
|
||||
/** |
||||
* Immediately process the given {@link ApplicationEvent}. In contrast to |
||||
* {@link #onApplicationEvent(ApplicationEvent)}, a call to this method will |
||||
* directly process the given event without deferring it to the associated |
||||
* {@link #getTransactionPhase() transaction phase}. |
||||
* @param event the event to process through the target listener implementation |
||||
*/ |
||||
void processEvent(E event); |
||||
|
||||
|
||||
/** |
||||
* Create a new {@code TransactionalApplicationListener} for the given payload consumer, |
||||
* to be applied in the default phase {@link TransactionPhase#AFTER_COMMIT}. |
||||
* @param consumer the event payload consumer |
||||
* @param <T> the type of the event payload |
||||
* @return a corresponding {@code TransactionalApplicationListener} instance |
||||
* @see PayloadApplicationEvent#getPayload() |
||||
* @see TransactionalApplicationListenerAdapter |
||||
*/ |
||||
static <T> TransactionalApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) { |
||||
return forPayload(TransactionPhase.AFTER_COMMIT, consumer); |
||||
} |
||||
|
||||
/** |
||||
* Create a new {@code TransactionalApplicationListener} for the given payload consumer. |
||||
* @param phase the transaction phase in which to invoke the listener |
||||
* @param consumer the event payload consumer |
||||
* @param <T> the type of the event payload |
||||
* @return a corresponding {@code TransactionalApplicationListener} instance |
||||
* @see PayloadApplicationEvent#getPayload() |
||||
* @see TransactionalApplicationListenerAdapter |
||||
*/ |
||||
static <T> TransactionalApplicationListener<PayloadApplicationEvent<T>> forPayload( |
||||
TransactionPhase phase, Consumer<T> consumer) { |
||||
|
||||
TransactionalApplicationListenerAdapter<PayloadApplicationEvent<T>> listener = |
||||
new TransactionalApplicationListenerAdapter<>(event -> consumer.accept(event.getPayload())); |
||||
listener.setTransactionPhase(phase); |
||||
return listener; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Callback to be invoked on synchronization-driven event processing, |
||||
* wrapping the target listener invocation ({@link #processEvent}). |
||||
* |
||||
* @see #addCallback |
||||
* @see #processEvent |
||||
*/ |
||||
interface SynchronizationCallback { |
||||
|
||||
/** |
||||
* Called before transactional event listener invocation. |
||||
* @param event the event that transaction synchronization is about to process |
||||
*/ |
||||
default void preProcessEvent(ApplicationEvent event) { |
||||
} |
||||
|
||||
/** |
||||
* Called after a transactional event listener invocation. |
||||
* @param event the event that transaction synchronization finished processing |
||||
* @param ex an exception that occurred during listener invocation, if any |
||||
*/ |
||||
default void postProcessEvent(ApplicationEvent event, @Nullable Throwable ex) { |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,138 @@
@@ -0,0 +1,138 @@
|
||||
/* |
||||
* Copyright 2002-2020 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.util.List; |
||||
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; |
||||
|
||||
/** |
||||
* {@link TransactionalApplicationListener} adapter that delegates the processing of |
||||
* an event to a target {@link ApplicationListener} instance. Supports the exact |
||||
* same features as any regular {@link ApplicationListener} but is aware of the |
||||
* transactional context of the event publisher. |
||||
* |
||||
* <p>For simple {@link org.springframework.context.PayloadApplicationEvent} handling, |
||||
* consider the {@link TransactionalApplicationListener#forPayload} factory methods |
||||
* as a convenient alternative to custom usage of this adapter class. |
||||
* |
||||
* @author Juergen Hoeller |
||||
* @since 5.3 |
||||
* @param <E> the specific {@code ApplicationEvent} subclass to listen to |
||||
* @see TransactionalApplicationListener |
||||
* @see TransactionalEventListener |
||||
* @see TransactionalApplicationListenerMethodAdapter |
||||
*/ |
||||
public class TransactionalApplicationListenerAdapter<E extends ApplicationEvent> |
||||
implements TransactionalApplicationListener<E>, Ordered { |
||||
|
||||
private final ApplicationListener<E> targetListener; |
||||
|
||||
private int order = Ordered.LOWEST_PRECEDENCE; |
||||
|
||||
private TransactionPhase transactionPhase = TransactionPhase.AFTER_COMMIT; |
||||
|
||||
private String listenerId = ""; |
||||
|
||||
private final List<SynchronizationCallback> callbacks = new CopyOnWriteArrayList<>(); |
||||
|
||||
|
||||
/** |
||||
* Construct a new TransactionalApplicationListenerAdapter. |
||||
* @param targetListener the actual listener to invoke in the specified transaction phase |
||||
* @see #setTransactionPhase |
||||
* @see TransactionalApplicationListener#forPayload |
||||
*/ |
||||
public TransactionalApplicationListenerAdapter(ApplicationListener<E> targetListener) { |
||||
this.targetListener = targetListener; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Specify the synchronization order for the listener. |
||||
*/ |
||||
public void setOrder(int order) { |
||||
this.order = order; |
||||
} |
||||
|
||||
/** |
||||
* Return the synchronization order for the listener. |
||||
*/ |
||||
@Override |
||||
public int getOrder() { |
||||
return this.order; |
||||
} |
||||
|
||||
/** |
||||
* Specify the transaction phase to invoke the listener in. |
||||
* <p>The default is {@link TransactionPhase#AFTER_COMMIT}. |
||||
*/ |
||||
public void setTransactionPhase(TransactionPhase transactionPhase) { |
||||
this.transactionPhase = transactionPhase; |
||||
} |
||||
|
||||
/** |
||||
* Return the transaction phase to invoke the listener in. |
||||
*/ |
||||
@Override |
||||
public TransactionPhase getTransactionPhase() { |
||||
return this.transactionPhase; |
||||
} |
||||
|
||||
/** |
||||
* Specify an id to identify the listener with. |
||||
* <p>The default is an empty String. |
||||
*/ |
||||
public void setListenerId(String listenerId) { |
||||
this.listenerId = listenerId; |
||||
} |
||||
|
||||
/** |
||||
* Return an id to identify the listener with. |
||||
*/ |
||||
@Override |
||||
public String getListenerId() { |
||||
return this.listenerId; |
||||
} |
||||
|
||||
@Override |
||||
public void addCallback(SynchronizationCallback callback) { |
||||
Assert.notNull(callback, "SynchronizationCallback must not be null"); |
||||
this.callbacks.add(callback); |
||||
} |
||||
|
||||
@Override |
||||
public void processEvent(E event) { |
||||
this.targetListener.onApplicationEvent(event); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public void onApplicationEvent(E event) { |
||||
if (TransactionSynchronizationManager.isSynchronizationActive() && |
||||
TransactionSynchronizationManager.isActualTransactionActive()) { |
||||
TransactionSynchronizationManager.registerSynchronization( |
||||
new TransactionalApplicationListenerSynchronization<>(event, this, this.callbacks)); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
|
||||
/* |
||||
* Copyright 2002-2020 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.util.List; |
||||
|
||||
import org.springframework.context.ApplicationEvent; |
||||
import org.springframework.transaction.support.TransactionSynchronization; |
||||
|
||||
/** |
||||
* {@link TransactionSynchronization} implementation for event processing with a |
||||
* {@link TransactionalApplicationListener}. |
||||
* |
||||
* @author Juergen Hoeller |
||||
* @since 5.3 |
||||
* @param <E> the specific {@code ApplicationEvent} subclass to listen to |
||||
*/ |
||||
class TransactionalApplicationListenerSynchronization<E extends ApplicationEvent> |
||||
implements TransactionSynchronization { |
||||
|
||||
private final E event; |
||||
|
||||
private final TransactionalApplicationListener<E> listener; |
||||
|
||||
private final List<TransactionalApplicationListener.SynchronizationCallback> callbacks; |
||||
|
||||
|
||||
public TransactionalApplicationListenerSynchronization(E event, TransactionalApplicationListener<E> listener, |
||||
List<TransactionalApplicationListener.SynchronizationCallback> callbacks) { |
||||
|
||||
this.event = event; |
||||
this.listener = listener; |
||||
this.callbacks = callbacks; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public int getOrder() { |
||||
return this.listener.getOrder(); |
||||
} |
||||
|
||||
@Override |
||||
public void beforeCommit(boolean readOnly) { |
||||
if (this.listener.getTransactionPhase() == TransactionPhase.BEFORE_COMMIT) { |
||||
processEventWithCallbacks(); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
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)); |
||||
try { |
||||
this.listener.processEvent(this.event); |
||||
} |
||||
catch (RuntimeException | Error ex) { |
||||
this.callbacks.forEach(callback -> callback.postProcessEvent(this.event, ex)); |
||||
throw ex; |
||||
} |
||||
this.callbacks.forEach(callback -> callback.postProcessEvent(this.event, null)); |
||||
} |
||||
|
||||
} |
||||
@ -1,106 +0,0 @@
@@ -1,106 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-2019 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.reflect.Method; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.context.PayloadApplicationEvent; |
||||
import org.springframework.context.event.ApplicationListenerMethodAdapter; |
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.core.annotation.AnnotatedElementUtils; |
||||
import org.springframework.util.ReflectionUtils; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* @author Stephane Nicoll |
||||
*/ |
||||
public class ApplicationListenerMethodTransactionalAdapterTests { |
||||
|
||||
@Test |
||||
public void defaultPhase() { |
||||
Method m = ReflectionUtils.findMethod(SampleEvents.class, "defaultPhase", String.class); |
||||
assertPhase(m, TransactionPhase.AFTER_COMMIT); |
||||
} |
||||
|
||||
@Test |
||||
public void phaseSet() { |
||||
Method m = ReflectionUtils.findMethod(SampleEvents.class, "phaseSet", String.class); |
||||
assertPhase(m, TransactionPhase.AFTER_ROLLBACK); |
||||
} |
||||
|
||||
@Test |
||||
public void phaseAndClassesSet() { |
||||
Method m = ReflectionUtils.findMethod(SampleEvents.class, "phaseAndClassesSet"); |
||||
assertPhase(m, TransactionPhase.AFTER_COMPLETION); |
||||
supportsEventType(true, m, createGenericEventType(String.class)); |
||||
supportsEventType(true, m, createGenericEventType(Integer.class)); |
||||
supportsEventType(false, m, createGenericEventType(Double.class)); |
||||
} |
||||
|
||||
@Test |
||||
public void valueSet() { |
||||
Method m = ReflectionUtils.findMethod(SampleEvents.class, "valueSet"); |
||||
assertPhase(m, TransactionPhase.AFTER_COMMIT); |
||||
supportsEventType(true, m, createGenericEventType(String.class)); |
||||
supportsEventType(false, m, createGenericEventType(Double.class)); |
||||
} |
||||
|
||||
private void assertPhase(Method method, TransactionPhase expected) { |
||||
assertThat(method).as("Method must not be null").isNotNull(); |
||||
TransactionalEventListener annotation = |
||||
AnnotatedElementUtils.findMergedAnnotation(method, TransactionalEventListener.class); |
||||
assertThat(annotation.phase()).as("Wrong phase for '" + method + "'").isEqualTo(expected); |
||||
} |
||||
|
||||
private void supportsEventType(boolean match, Method method, ResolvableType eventType) { |
||||
ApplicationListenerMethodAdapter adapter = createTestInstance(method); |
||||
assertThat(adapter.supportsEventType(eventType)).as("Wrong match for event '" + eventType + "' on " + method).isEqualTo(match); |
||||
} |
||||
|
||||
private ApplicationListenerMethodTransactionalAdapter createTestInstance(Method m) { |
||||
return new ApplicationListenerMethodTransactionalAdapter("test", SampleEvents.class, m); |
||||
} |
||||
|
||||
private ResolvableType createGenericEventType(Class<?> payloadType) { |
||||
return ResolvableType.forClassWithGenerics(PayloadApplicationEvent.class, payloadType); |
||||
} |
||||
|
||||
|
||||
static class SampleEvents { |
||||
|
||||
@TransactionalEventListener |
||||
public void defaultPhase(String data) { |
||||
} |
||||
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) |
||||
public void phaseSet(String data) { |
||||
} |
||||
|
||||
@TransactionalEventListener(classes = {String.class, Integer.class}, |
||||
phase = TransactionPhase.AFTER_COMPLETION) |
||||
public void phaseAndClassesSet() { |
||||
} |
||||
|
||||
@TransactionalEventListener(String.class) |
||||
public void valueSet() { |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
/* |
||||
* Copyright 2002-2020 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 org.springframework.context.ApplicationEvent; |
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* @author Juergen Hoeller |
||||
* @author Oliver Drotbohm |
||||
*/ |
||||
class CapturingSynchronizationCallback implements TransactionalApplicationListener.SynchronizationCallback { |
||||
|
||||
@Nullable |
||||
ApplicationEvent preEvent; |
||||
|
||||
@Nullable |
||||
ApplicationEvent postEvent; |
||||
|
||||
@Nullable |
||||
Throwable ex; |
||||
|
||||
@Override |
||||
public void preProcessEvent(ApplicationEvent event) { |
||||
this.preEvent = event; |
||||
} |
||||
|
||||
@Override |
||||
public void postProcessEvent(ApplicationEvent event, @Nullable Throwable ex) { |
||||
this.postEvent = event; |
||||
this.ex = ex; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,109 @@
@@ -0,0 +1,109 @@
|
||||
/* |
||||
* Copyright 2002-2020 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 org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.context.PayloadApplicationEvent; |
||||
import org.springframework.transaction.support.TransactionSynchronization; |
||||
import org.springframework.transaction.support.TransactionSynchronizationManager; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
|
||||
/** |
||||
* @author Juergen Hoeller |
||||
*/ |
||||
public class TransactionalApplicationListenerAdapterTests { |
||||
|
||||
@Test |
||||
public void invokesCompletionCallbackOnSuccess() { |
||||
CapturingSynchronizationCallback callback = new CapturingSynchronizationCallback(); |
||||
PayloadApplicationEvent<Object> event = new PayloadApplicationEvent<>(this, new Object()); |
||||
|
||||
TransactionalApplicationListener<PayloadApplicationEvent<Object>> adapter = |
||||
TransactionalApplicationListener.forPayload(p -> {}); |
||||
adapter.addCallback(callback); |
||||
runInTransaction(() -> adapter.onApplicationEvent(event)); |
||||
|
||||
assertThat(callback.preEvent).isEqualTo(event); |
||||
assertThat(callback.postEvent).isEqualTo(event); |
||||
assertThat(callback.ex).isNull(); |
||||
assertThat(adapter.getTransactionPhase()).isEqualTo(TransactionPhase.AFTER_COMMIT); |
||||
assertThat(adapter.getListenerId()).isEqualTo(""); |
||||
} |
||||
|
||||
@Test |
||||
public void invokesExceptionHandlerOnException() { |
||||
CapturingSynchronizationCallback callback = new CapturingSynchronizationCallback(); |
||||
PayloadApplicationEvent<String> event = new PayloadApplicationEvent<>(this, "event"); |
||||
RuntimeException ex = new RuntimeException("event"); |
||||
|
||||
TransactionalApplicationListener<PayloadApplicationEvent<String>> adapter = |
||||
TransactionalApplicationListener.forPayload( |
||||
TransactionPhase.BEFORE_COMMIT, p -> {throw ex;}); |
||||
adapter.addCallback(callback); |
||||
|
||||
assertThatExceptionOfType(RuntimeException.class) |
||||
.isThrownBy(() -> runInTransaction(() -> adapter.onApplicationEvent(event))) |
||||
.withMessage("event"); |
||||
|
||||
assertThat(callback.preEvent).isEqualTo(event); |
||||
assertThat(callback.postEvent).isEqualTo(event); |
||||
assertThat(callback.ex).isEqualTo(ex); |
||||
assertThat(adapter.getTransactionPhase()).isEqualTo(TransactionPhase.BEFORE_COMMIT); |
||||
assertThat(adapter.getListenerId()).isEqualTo(""); |
||||
} |
||||
|
||||
@Test |
||||
public void useSpecifiedIdentifier() { |
||||
CapturingSynchronizationCallback callback = new CapturingSynchronizationCallback(); |
||||
PayloadApplicationEvent<String> event = new PayloadApplicationEvent<>(this, "event"); |
||||
|
||||
TransactionalApplicationListenerAdapter<PayloadApplicationEvent<String>> adapter = |
||||
new TransactionalApplicationListenerAdapter<>(e -> {}); |
||||
adapter.setTransactionPhase(TransactionPhase.BEFORE_COMMIT); |
||||
adapter.setListenerId("identifier"); |
||||
adapter.addCallback(callback); |
||||
runInTransaction(() -> adapter.onApplicationEvent(event)); |
||||
|
||||
assertThat(callback.preEvent).isEqualTo(event); |
||||
assertThat(callback.postEvent).isEqualTo(event); |
||||
assertThat(callback.ex).isNull(); |
||||
assertThat(adapter.getTransactionPhase()).isEqualTo(TransactionPhase.BEFORE_COMMIT); |
||||
assertThat(adapter.getListenerId()).isEqualTo("identifier"); |
||||
} |
||||
|
||||
|
||||
private static void runInTransaction(Runnable runnable) { |
||||
TransactionSynchronizationManager.setActualTransactionActive(true); |
||||
TransactionSynchronizationManager.initSynchronization(); |
||||
try { |
||||
runnable.run(); |
||||
TransactionSynchronizationManager.getSynchronizations().forEach(it -> { |
||||
it.beforeCommit(false); |
||||
it.afterCommit(); |
||||
it.afterCompletion(TransactionSynchronization.STATUS_COMMITTED); |
||||
}); |
||||
} |
||||
finally { |
||||
TransactionSynchronizationManager.clearSynchronization(); |
||||
TransactionSynchronizationManager.setActualTransactionActive(false); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,198 @@
@@ -0,0 +1,198 @@
|
||||
/* |
||||
* Copyright 2002-2020 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.reflect.Method; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.context.PayloadApplicationEvent; |
||||
import org.springframework.context.event.ApplicationListenerMethodAdapter; |
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.core.annotation.AnnotatedElementUtils; |
||||
import org.springframework.transaction.support.TransactionSynchronization; |
||||
import org.springframework.transaction.support.TransactionSynchronizationManager; |
||||
import org.springframework.util.ReflectionUtils; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
|
||||
/** |
||||
* @author Stephane Nicoll |
||||
* @author Juergen Hoeller |
||||
* @author Oliver Drotbohm |
||||
*/ |
||||
public class TransactionalApplicationListenerMethodAdapterTests { |
||||
|
||||
@Test |
||||
public void defaultPhase() { |
||||
Method m = ReflectionUtils.findMethod(SampleEvents.class, "defaultPhase", String.class); |
||||
assertPhase(m, TransactionPhase.AFTER_COMMIT); |
||||
} |
||||
|
||||
@Test |
||||
public void phaseSet() { |
||||
Method m = ReflectionUtils.findMethod(SampleEvents.class, "phaseSet", String.class); |
||||
assertPhase(m, TransactionPhase.AFTER_ROLLBACK); |
||||
} |
||||
|
||||
@Test |
||||
public void phaseAndClassesSet() { |
||||
Method m = ReflectionUtils.findMethod(SampleEvents.class, "phaseAndClassesSet"); |
||||
assertPhase(m, TransactionPhase.AFTER_COMPLETION); |
||||
supportsEventType(true, m, createGenericEventType(String.class)); |
||||
supportsEventType(true, m, createGenericEventType(Integer.class)); |
||||
supportsEventType(false, m, createGenericEventType(Double.class)); |
||||
} |
||||
|
||||
@Test |
||||
public void valueSet() { |
||||
Method m = ReflectionUtils.findMethod(SampleEvents.class, "valueSet"); |
||||
assertPhase(m, TransactionPhase.AFTER_COMMIT); |
||||
supportsEventType(true, m, createGenericEventType(String.class)); |
||||
supportsEventType(false, m, createGenericEventType(Double.class)); |
||||
} |
||||
|
||||
@Test |
||||
public void invokesCompletionCallbackOnSuccess() { |
||||
Method m = ReflectionUtils.findMethod(SampleEvents.class, "defaultPhase", String.class); |
||||
CapturingSynchronizationCallback callback = new CapturingSynchronizationCallback(); |
||||
PayloadApplicationEvent<Object> event = new PayloadApplicationEvent<>(this, new Object()); |
||||
|
||||
TransactionalApplicationListenerMethodAdapter adapter = createTestInstance(m); |
||||
adapter.addCallback(callback); |
||||
runInTransaction(() -> adapter.onApplicationEvent(event)); |
||||
|
||||
assertThat(callback.preEvent).isEqualTo(event); |
||||
assertThat(callback.postEvent).isEqualTo(event); |
||||
assertThat(callback.ex).isNull(); |
||||
assertThat(adapter.getTransactionPhase()).isEqualTo(TransactionPhase.AFTER_COMMIT); |
||||
assertThat(adapter.getListenerId()).endsWith("SampleEvents.defaultPhase(class java.lang.String)"); |
||||
} |
||||
|
||||
@Test |
||||
public void invokesExceptionHandlerOnException() { |
||||
Method m = ReflectionUtils.findMethod(SampleEvents.class, "throwing", String.class); |
||||
CapturingSynchronizationCallback callback = new CapturingSynchronizationCallback(); |
||||
PayloadApplicationEvent<String> event = new PayloadApplicationEvent<>(this, "event"); |
||||
|
||||
TransactionalApplicationListenerMethodAdapter adapter = createTestInstance(m); |
||||
adapter.addCallback(callback); |
||||
|
||||
assertThatExceptionOfType(RuntimeException.class) |
||||
.isThrownBy(() -> runInTransaction(() -> adapter.onApplicationEvent(event))) |
||||
.withMessage("event"); |
||||
|
||||
assertThat(callback.preEvent).isEqualTo(event); |
||||
assertThat(callback.postEvent).isEqualTo(event); |
||||
assertThat(callback.ex).isInstanceOf(RuntimeException.class); |
||||
assertThat(callback.ex.getMessage()).isEqualTo("event"); |
||||
assertThat(adapter.getTransactionPhase()).isEqualTo(TransactionPhase.BEFORE_COMMIT); |
||||
assertThat(adapter.getListenerId()).isEqualTo(adapter.getDefaultListenerId()); |
||||
} |
||||
|
||||
@Test |
||||
public void usesAnnotatedIdentifier() { |
||||
Method m = ReflectionUtils.findMethod(SampleEvents.class, "identified", String.class); |
||||
CapturingSynchronizationCallback callback = new CapturingSynchronizationCallback(); |
||||
PayloadApplicationEvent<String> event = new PayloadApplicationEvent<>(this, "event"); |
||||
|
||||
TransactionalApplicationListenerMethodAdapter adapter = createTestInstance(m); |
||||
adapter.addCallback(callback); |
||||
runInTransaction(() -> adapter.onApplicationEvent(event)); |
||||
|
||||
assertThat(callback.preEvent).isEqualTo(event); |
||||
assertThat(callback.postEvent).isEqualTo(event); |
||||
assertThat(callback.ex).isNull(); |
||||
assertThat(adapter.getTransactionPhase()).isEqualTo(TransactionPhase.AFTER_COMMIT); |
||||
assertThat(adapter.getListenerId()).endsWith("identifier"); |
||||
} |
||||
|
||||
|
||||
private static void assertPhase(Method method, TransactionPhase expected) { |
||||
assertThat(method).as("Method must not be null").isNotNull(); |
||||
TransactionalEventListener annotation = |
||||
AnnotatedElementUtils.findMergedAnnotation(method, TransactionalEventListener.class); |
||||
assertThat(annotation.phase()).as("Wrong phase for '" + method + "'").isEqualTo(expected); |
||||
} |
||||
|
||||
private static void supportsEventType(boolean match, Method method, ResolvableType eventType) { |
||||
ApplicationListenerMethodAdapter adapter = createTestInstance(method); |
||||
assertThat(adapter.supportsEventType(eventType)).as("Wrong match for event '" + eventType + "' on " + method).isEqualTo(match); |
||||
} |
||||
|
||||
private static TransactionalApplicationListenerMethodAdapter createTestInstance(Method m) { |
||||
return new TransactionalApplicationListenerMethodAdapter("test", SampleEvents.class, m) { |
||||
@Override |
||||
protected Object getTargetBean() { |
||||
return new SampleEvents(); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
private static ResolvableType createGenericEventType(Class<?> payloadType) { |
||||
return ResolvableType.forClassWithGenerics(PayloadApplicationEvent.class, payloadType); |
||||
} |
||||
|
||||
private static void runInTransaction(Runnable runnable) { |
||||
TransactionSynchronizationManager.setActualTransactionActive(true); |
||||
TransactionSynchronizationManager.initSynchronization(); |
||||
try { |
||||
runnable.run(); |
||||
TransactionSynchronizationManager.getSynchronizations().forEach(it -> { |
||||
it.beforeCommit(false); |
||||
it.afterCommit(); |
||||
it.afterCompletion(TransactionSynchronization.STATUS_COMMITTED); |
||||
}); |
||||
} |
||||
finally { |
||||
TransactionSynchronizationManager.clearSynchronization(); |
||||
TransactionSynchronizationManager.setActualTransactionActive(false); |
||||
} |
||||
} |
||||
|
||||
|
||||
static class SampleEvents { |
||||
|
||||
@TransactionalEventListener |
||||
public void defaultPhase(String data) { |
||||
} |
||||
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK) |
||||
public void phaseSet(String data) { |
||||
} |
||||
|
||||
@TransactionalEventListener(classes = {String.class, Integer.class}, |
||||
phase = TransactionPhase.AFTER_COMPLETION) |
||||
public void phaseAndClassesSet() { |
||||
} |
||||
|
||||
@TransactionalEventListener(String.class) |
||||
public void valueSet() { |
||||
} |
||||
|
||||
@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) |
||||
public void throwing(String data) { |
||||
throw new RuntimeException(data); |
||||
} |
||||
|
||||
@TransactionalEventListener(id = "identifier") |
||||
public void identified(String data) { |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue