Manage asynchronous EventListener with replies
This commit makes sure to reject an `@EventListener` annotated method that also uses `@Async`. In such scenario, the method is invoked in a separate thread and the infrastructure has no handle on the actual reply, if any. The documentation has been improved to refer to that scenario. Issue: SPR-14113
This commit is contained in:
parent
44a9c495ab
commit
bee1b77af5
|
|
@ -35,8 +35,10 @@ import org.springframework.context.expression.AnnotatedElementKey;
|
||||||
import org.springframework.core.BridgeMethodResolver;
|
import org.springframework.core.BridgeMethodResolver;
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||||
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.expression.EvaluationContext;
|
import org.springframework.expression.EvaluationContext;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.util.ReflectionUtils;
|
import org.springframework.util.ReflectionUtils;
|
||||||
|
|
@ -82,6 +84,7 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
|
||||||
|
|
||||||
|
|
||||||
public ApplicationListenerMethodAdapter(String beanName, Class<?> targetClass, Method method) {
|
public ApplicationListenerMethodAdapter(String beanName, Class<?> targetClass, Method method) {
|
||||||
|
validateMethod(method);
|
||||||
this.beanName = beanName;
|
this.beanName = beanName;
|
||||||
this.method = method;
|
this.method = method;
|
||||||
this.targetClass = targetClass;
|
this.targetClass = targetClass;
|
||||||
|
|
@ -90,6 +93,14 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
|
||||||
this.methodKey = new AnnotatedElementKey(this.method, this.targetClass);
|
this.methodKey = new AnnotatedElementKey(this.method, this.targetClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void validateMethod(Method method) {
|
||||||
|
if (method.getReturnType() != void.class &&
|
||||||
|
AnnotationUtils.findAnnotation(method, Async.class) != null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Asynchronous @EventListener method is not allowed to return reply events: " + method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize this instance.
|
* Initialize this instance.
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,19 @@ public class AnnotationDrivenEventListenerTests {
|
||||||
failingContext.refresh();
|
failingContext.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void asyncWithReplyEventListener() {
|
||||||
|
AnnotationConfigApplicationContext failingContext =
|
||||||
|
new AnnotationConfigApplicationContext();
|
||||||
|
failingContext.register(BasicConfiguration.class,
|
||||||
|
InvalidAsyncEventListener.class);
|
||||||
|
|
||||||
|
this.thrown.expect(BeanInitializationException.class);
|
||||||
|
this.thrown.expectMessage(InvalidAsyncEventListener.class.getName());
|
||||||
|
this.thrown.expectMessage("asyncCannotUseReply");
|
||||||
|
failingContext.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void simpleReply() {
|
public void simpleReply() {
|
||||||
load(TestEventListener.class, ReplyEventListener.class);
|
load(TestEventListener.class, ReplyEventListener.class);
|
||||||
|
|
@ -656,6 +669,17 @@ public class AnnotationDrivenEventListenerTests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component
|
||||||
|
static class InvalidAsyncEventListener {
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
@Async
|
||||||
|
public Integer asyncCannotUseReply(String payload) {
|
||||||
|
return 42;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
static class ReplyEventListener extends AbstractTestEventListener {
|
static class ReplyEventListener extends AbstractTestEventListener {
|
||||||
|
|
@ -766,6 +790,7 @@ public class AnnotationDrivenEventListenerTests {
|
||||||
this.eventCollector.addEvent(this, event);
|
this.eventCollector.addEvent(this, event);
|
||||||
this.countDownLatch.countDown();
|
this.countDownLatch.countDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -869,7 +894,6 @@ public class AnnotationDrivenEventListenerTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@EventListener
|
@EventListener
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
public @interface ConditionalEvent {
|
public @interface ConditionalEvent {
|
||||||
|
|
|
||||||
|
|
@ -8208,11 +8208,42 @@ method signature to return the event that should be published, something like:
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
|
NOTE: This feature is not supported for <<context-functionality-events-async,asynchronous
|
||||||
|
listeners>>.
|
||||||
|
|
||||||
This new method will publish a new `ListUpdateEvent` for every `BlackListEvent` handled
|
This new method will publish a new `ListUpdateEvent` for every `BlackListEvent` handled
|
||||||
by the method above. If you need to publish several events, just return a `Collection` of
|
by the method above. If you need to publish several events, just return a `Collection` of
|
||||||
events instead.
|
events instead.
|
||||||
|
|
||||||
Finally if you need the listener to be invoked before another one, just add the `@Order`
|
[[context-functionality-events-async]]
|
||||||
|
==== Asynchronous Listeners
|
||||||
|
|
||||||
|
If you want a particular listener to process events asynchronously, simply reuse the
|
||||||
|
<<scheduling-annotation-support-async,regular `@Async` support>>:
|
||||||
|
|
||||||
|
[source,java,indent=0]
|
||||||
|
[subs="verbatim,quotes"]
|
||||||
|
----
|
||||||
|
@EventListener
|
||||||
|
@Async
|
||||||
|
public void processBlackListEvent(BlackListEvent event) {
|
||||||
|
// BlackListEvent is processed in a separate thread
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
Be aware of the following limitations when using asynchronous events:
|
||||||
|
|
||||||
|
. If the event listener throws an `Exception` it will not be propagated to the caller,
|
||||||
|
check `AsyncUncaughtExceptionHandler` for more details.
|
||||||
|
. Such event listener cannot send replies. If you need to send another event as the
|
||||||
|
result of the processing, inject `ApplicationEventPublisher` to send the event
|
||||||
|
manually.
|
||||||
|
|
||||||
|
|
||||||
|
[[context-functionality-events-order]
|
||||||
|
==== Ordering Listeners
|
||||||
|
|
||||||
|
If you need the listener to be invoked before another one, just add the `@Order`
|
||||||
annotation to the method declaration:
|
annotation to the method declaration:
|
||||||
|
|
||||||
[source,java,indent=0]
|
[source,java,indent=0]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue