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:
Stephane Nicoll 2016-04-12 09:06:48 +02:00
parent 44a9c495ab
commit bee1b77af5
3 changed files with 68 additions and 2 deletions

View File

@ -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.

View File

@ -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 {

View File

@ -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]