Support for multiple events per method
In addition to specifying the event type to listen to via a method
parameter, any @EventListener annotated method can now alternatively
define the event type(s) to listen to via the "classes" attributes (that
is aliased to "value").
Something like
@EventListener({FooEvent.class, BarEvent.class})
public void handleFooBar() { .... }
Issue: SPR-13156
This commit is contained in:
@@ -20,7 +20,10 @@ import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -66,7 +69,7 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
|
||||
|
||||
private final Method bridgedMethod;
|
||||
|
||||
private final ResolvableType declaredEventType;
|
||||
private final List<ResolvableType> declaredEventTypes;
|
||||
|
||||
private final AnnotatedElementKey methodKey;
|
||||
|
||||
@@ -76,12 +79,14 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
|
||||
|
||||
private String condition;
|
||||
|
||||
private EventListener eventListener;
|
||||
|
||||
public ApplicationListenerMethodAdapter(String beanName, Class<?> targetClass, Method method) {
|
||||
this.beanName = beanName;
|
||||
this.method = method;
|
||||
this.targetClass = targetClass;
|
||||
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
|
||||
this.declaredEventType = resolveDeclaredEventType();
|
||||
this.declaredEventTypes = resolveDeclaredEventTypes();
|
||||
this.methodKey = new AnnotatedElementKey(this.method, this.targetClass);
|
||||
}
|
||||
|
||||
@@ -122,19 +127,20 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
|
||||
* therefore the method should not be invoked at all for the specified event.
|
||||
*/
|
||||
protected Object[] resolveArguments(ApplicationEvent event) {
|
||||
if (!ApplicationEvent.class.isAssignableFrom(this.declaredEventType.getRawClass())
|
||||
ResolvableType declaredEventType = getResolvableType(event);
|
||||
if (declaredEventType == null) {
|
||||
return null;
|
||||
}
|
||||
if (this.method.getParameters().length == 0) {
|
||||
return new Object[0];
|
||||
}
|
||||
if (!ApplicationEvent.class.isAssignableFrom(declaredEventType.getRawClass())
|
||||
&& event instanceof PayloadApplicationEvent) {
|
||||
PayloadApplicationEvent<?> payloadEvent = (PayloadApplicationEvent<?>) event;
|
||||
ResolvableType payloadType = payloadEvent.getResolvableType()
|
||||
.as(PayloadApplicationEvent.class).getGeneric(0);
|
||||
if (this.declaredEventType.isAssignableFrom(payloadType)) {
|
||||
return new Object[] {payloadEvent.getPayload()};
|
||||
}
|
||||
return new Object[] {((PayloadApplicationEvent) event).getPayload()};
|
||||
}
|
||||
else {
|
||||
return new Object[] {event};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void handleResult(Object result) {
|
||||
@@ -178,14 +184,18 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
|
||||
|
||||
@Override
|
||||
public boolean supportsEventType(ResolvableType eventType) {
|
||||
if (this.declaredEventType.isAssignableFrom(eventType)) {
|
||||
return true;
|
||||
for (ResolvableType declaredEventType : this.declaredEventTypes) {
|
||||
if (declaredEventType.isAssignableFrom(eventType)) {
|
||||
return true;
|
||||
}
|
||||
else if (PayloadApplicationEvent.class.isAssignableFrom(eventType.getRawClass())) {
|
||||
ResolvableType payloadType = eventType.as(PayloadApplicationEvent.class).getGeneric();
|
||||
if (declaredEventType.isAssignableFrom(payloadType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (PayloadApplicationEvent.class.isAssignableFrom(eventType.getRawClass())) {
|
||||
ResolvableType payloadType = eventType.as(PayloadApplicationEvent.class).getGeneric();
|
||||
return eventType.hasUnresolvableGenerics() || this.declaredEventType.isAssignableFrom(payloadType);
|
||||
}
|
||||
return false;
|
||||
return eventType.hasUnresolvableGenerics();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -240,6 +250,13 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
|
||||
return this.applicationContext.getBean(this.beanName);
|
||||
}
|
||||
|
||||
protected EventListener getEventListener() {
|
||||
if (this.eventListener == null) {
|
||||
this.eventListener = AnnotatedElementUtils.findMergedAnnotation(this.method, EventListener.class);
|
||||
}
|
||||
return this.eventListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the condition to use.
|
||||
* <p>Matches the {@code condition} attribute of the {@link EventListener}
|
||||
@@ -305,13 +322,48 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
|
||||
}
|
||||
|
||||
|
||||
private ResolvableType resolveDeclaredEventType() {
|
||||
private ResolvableType getResolvableType(ApplicationEvent event) {
|
||||
ResolvableType payloadType = null;
|
||||
if (event instanceof PayloadApplicationEvent) {
|
||||
PayloadApplicationEvent<?> payloadEvent = (PayloadApplicationEvent<?>) event;
|
||||
payloadType = payloadEvent.getResolvableType().as(
|
||||
PayloadApplicationEvent.class).getGeneric(0);
|
||||
}
|
||||
for (ResolvableType declaredEventType : this.declaredEventTypes) {
|
||||
if (!ApplicationEvent.class.isAssignableFrom(declaredEventType.getRawClass())
|
||||
&& payloadType != null) {
|
||||
if (declaredEventType.isAssignableFrom(payloadType)) {
|
||||
return declaredEventType;
|
||||
}
|
||||
}
|
||||
if (declaredEventType.getRawClass().isAssignableFrom(event.getClass())) {
|
||||
return declaredEventType;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<ResolvableType> resolveDeclaredEventTypes() {
|
||||
int count = this.method.getParameterTypes().length;
|
||||
if (count != 1) {
|
||||
throw new IllegalStateException("Only one parameter is allowed " +
|
||||
if (count > 1) {
|
||||
throw new IllegalStateException("Maximum one parameter is allowed " +
|
||||
"for event listener method: " + method);
|
||||
}
|
||||
return ResolvableType.forMethodParameter(this.method, 0);
|
||||
EventListener ann = getEventListener();
|
||||
if (ann != null && ann.classes().length > 0) {
|
||||
List<ResolvableType> types = new ArrayList<ResolvableType>();
|
||||
for (Class<?> eventType : ann.classes()) {
|
||||
types.add(ResolvableType.forClass(eventType));
|
||||
}
|
||||
return types;
|
||||
}
|
||||
else {
|
||||
if (count == 0) {
|
||||
throw new IllegalStateException("Event parameter is mandatory " +
|
||||
"for event listener method: " + method);
|
||||
}
|
||||
return Collections.singletonList(ResolvableType.forMethodParameter(this.method, 0));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -23,12 +23,14 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Annotation that marks a method to listen for application events. The
|
||||
* method should have one and only one parameter that reflects the event
|
||||
* type to listen to. Events can be {@link ApplicationEvent} instances
|
||||
* as well as arbitrary objects.
|
||||
* method may have one (and only one) parameter that reflects the event
|
||||
* type to listen to. Or this annotation may refer to the event type(s)
|
||||
* using the {@link #classes()} attribute. Events can be {@link ApplicationEvent}
|
||||
* instances as well as arbitrary objects.
|
||||
*
|
||||
* <p>Processing of {@code @EventListener} annotations is performed via
|
||||
* {@link EventListenerMethodProcessor} that is registered automatically
|
||||
@@ -54,6 +56,21 @@ import org.springframework.context.ApplicationEvent;
|
||||
@Documented
|
||||
public @interface EventListener {
|
||||
|
||||
/**
|
||||
* Alias for {@link #classes()}.
|
||||
*/
|
||||
@AliasFor(attribute = "classes")
|
||||
Class<?>[] value() default {};
|
||||
|
||||
/**
|
||||
* The event classes that this listener handles. When this attribute is specified
|
||||
* with one value, the method parameter may or may not be specified. When this
|
||||
* attribute is specified with more than one value, the method must not have a
|
||||
* parameter.
|
||||
*/
|
||||
@AliasFor(attribute = "value")
|
||||
Class<?>[] classes() default {};
|
||||
|
||||
/**
|
||||
* Spring Expression Language (SpEL) attribute used for making the event
|
||||
* handling conditional.
|
||||
|
||||
@@ -113,6 +113,36 @@ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEv
|
||||
supportsEventType(true, method, ResolvableType.forClass(PayloadStringTestEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listenerWithAnnotationValue() {
|
||||
Method method = ReflectionUtils.findMethod(SampleEvents.class,
|
||||
"handleStringAnnotationValue");
|
||||
supportsEventType(true, method, createGenericEventType(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listenerWithAnnotationClasses() {
|
||||
Method method = ReflectionUtils.findMethod(SampleEvents.class,
|
||||
"handleStringAnnotationClasses");
|
||||
supportsEventType(true, method, createGenericEventType(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listenerWithAnnotationValueAndParameter() {
|
||||
Method method = ReflectionUtils.findMethod(SampleEvents.class,
|
||||
"handleStringAnnotationValueAndParameter", String.class);
|
||||
supportsEventType(true, method, createGenericEventType(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listenerWithSeveralTypes() {
|
||||
Method method = ReflectionUtils.findMethod(SampleEvents.class,
|
||||
"handleStringOrInteger");
|
||||
supportsEventType(true, method, createGenericEventType(String.class));
|
||||
supportsEventType(true, method, createGenericEventType(Integer.class));
|
||||
supportsEventType(false, method, createGenericEventType(Double.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listenerWithTooManyParameters() {
|
||||
Method method = ReflectionUtils.findMethod(SampleEvents.class,
|
||||
@@ -131,6 +161,15 @@ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEv
|
||||
createTestInstance(method);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listenerWithMoreThanOneParameter() {
|
||||
Method method = ReflectionUtils.findMethod(SampleEvents.class,
|
||||
"moreThanOneParameter", String.class, Integer.class);
|
||||
|
||||
thrown.expect(IllegalStateException.class);
|
||||
createTestInstance(method);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultOrder() {
|
||||
Method method = ReflectionUtils.findMethod(SampleEvents.class,
|
||||
@@ -249,6 +288,40 @@ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEv
|
||||
verify(this.sampleEvents, never()).handleString(anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokeListenerWithAnnotationValue() {
|
||||
Method method = ReflectionUtils.findMethod(SampleEvents.class,
|
||||
"handleStringAnnotationClasses");
|
||||
PayloadApplicationEvent<String> event = new PayloadApplicationEvent<>(this, "test");
|
||||
invokeListener(method, event);
|
||||
verify(this.sampleEvents, times(1)).handleStringAnnotationClasses();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokeListenerWithAnnotationValueAndParameter() {
|
||||
Method method = ReflectionUtils.findMethod(SampleEvents.class,
|
||||
"handleStringAnnotationValueAndParameter", String.class);
|
||||
PayloadApplicationEvent<String> event = new PayloadApplicationEvent<>(this, "test");
|
||||
invokeListener(method, event);
|
||||
verify(this.sampleEvents, times(1)).handleStringAnnotationValueAndParameter("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invokeListenerWithSeveralTypes() {
|
||||
Method method = ReflectionUtils.findMethod(SampleEvents.class,
|
||||
"handleStringOrInteger");
|
||||
PayloadApplicationEvent<String> event = new PayloadApplicationEvent<>(this, "test");
|
||||
invokeListener(method, event);
|
||||
verify(this.sampleEvents, times(1)).handleStringOrInteger();
|
||||
PayloadApplicationEvent<Integer> event2 = new PayloadApplicationEvent<>(this, 123);
|
||||
invokeListener(method, event2);
|
||||
verify(this.sampleEvents, times(2)).handleStringOrInteger();
|
||||
PayloadApplicationEvent<Double> event3 = new PayloadApplicationEvent<>(this, 23.2);
|
||||
invokeListener(method, event3);
|
||||
verify(this.sampleEvents, times(2)).handleStringOrInteger();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void beanInstanceRetrievedAtEveryInvocation() {
|
||||
Method method = ReflectionUtils.findMethod(SampleEvents.class,
|
||||
@@ -321,14 +394,32 @@ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEv
|
||||
public void handleString(String payload) {
|
||||
}
|
||||
|
||||
@EventListener(String.class)
|
||||
public void handleStringAnnotationValue() {
|
||||
}
|
||||
|
||||
@EventListener(classes = String.class)
|
||||
public void handleStringAnnotationClasses() {
|
||||
}
|
||||
|
||||
@EventListener(String.class)
|
||||
public void handleStringAnnotationValueAndParameter(String payload) {
|
||||
}
|
||||
|
||||
@EventListener({String.class, Integer.class})
|
||||
public void handleStringOrInteger() {
|
||||
}
|
||||
|
||||
@EventListener({String.class, Integer.class})
|
||||
public void handleStringOrIntegerWithParam(String invalid) {
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void handleGenericStringPayload(EntityWrapper<String> event) {
|
||||
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void handleGenericAnyPayload(EntityWrapper<?> event) {
|
||||
|
||||
}
|
||||
|
||||
@EventListener
|
||||
@@ -339,6 +430,10 @@ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEv
|
||||
public void noParameter() {
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void moreThanOneParameter(String foo, Integer bar) {
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void generateRuntimeException(GenericTestEvent<String> event) {
|
||||
if ("fail".equals(event.getPayload())) {
|
||||
|
||||
@@ -25,7 +25,7 @@ import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.event.ApplicationListenerMethodAdapter;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.context.event.GenericApplicationListener;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
@@ -80,8 +80,8 @@ class ApplicationListenerMethodTransactionalAdapter extends ApplicationListenerM
|
||||
}
|
||||
|
||||
static TransactionalEventListener findAnnotation(Method method) {
|
||||
TransactionalEventListener annotation = AnnotationUtils
|
||||
.findAnnotation(method, TransactionalEventListener.class);
|
||||
TransactionalEventListener annotation = AnnotatedElementUtils
|
||||
.findMergedAnnotation(method, TransactionalEventListener.class);
|
||||
if (annotation == null) {
|
||||
throw new IllegalStateException("No TransactionalEventListener annotation found on '" + method + "'");
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* An {@link EventListener} that is invoked according to a {@link TransactionPhase}.
|
||||
@@ -55,6 +56,21 @@ public @interface TransactionalEventListener {
|
||||
*/
|
||||
boolean fallbackExecution() default false;
|
||||
|
||||
/**
|
||||
* Alias for {@link #classes()}.
|
||||
*/
|
||||
@AliasFor(attribute = "classes")
|
||||
Class<?>[] value() default {};
|
||||
|
||||
/**
|
||||
* The event classes that this listener handles. When this attribute is specified
|
||||
* with one value, the method parameter may or may not be specified. When this
|
||||
* attribute is specified with more than one value, the method must not have a
|
||||
* parameter.
|
||||
*/
|
||||
@AliasFor(attribute = "value")
|
||||
Class<?>[] classes() default {};
|
||||
|
||||
/**
|
||||
* Spring Expression Language (SpEL) attribute used for making the event
|
||||
* handling conditional.
|
||||
|
||||
@@ -22,6 +22,9 @@ import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.context.PayloadApplicationEvent;
|
||||
import org.springframework.context.event.ApplicationListenerMethodAdapter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
@@ -36,7 +39,7 @@ public class ApplicationListenerMethodTransactionalAdapterTests {
|
||||
|
||||
@Test
|
||||
public void noAnnotation() {
|
||||
Method m = ReflectionUtils.findMethod(PhaseConfigurationTestListener.class,
|
||||
Method m = ReflectionUtils.findMethod(SampleEvents.class,
|
||||
"noAnnotation", String.class);
|
||||
|
||||
thrown.expect(IllegalStateException.class);
|
||||
@@ -46,24 +49,54 @@ public class ApplicationListenerMethodTransactionalAdapterTests {
|
||||
|
||||
@Test
|
||||
public void defaultPhase() {
|
||||
Method m = ReflectionUtils.findMethod(PhaseConfigurationTestListener.class, "defaultPhase", String.class);
|
||||
Method m = ReflectionUtils.findMethod(SampleEvents.class, "defaultPhase", String.class);
|
||||
assertPhase(m, TransactionPhase.AFTER_COMMIT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void phaseSet() {
|
||||
Method m = ReflectionUtils.findMethod(PhaseConfigurationTestListener.class, "phaseSet", String.class);
|
||||
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) {
|
||||
assertNotNull("Method must not be null", method);
|
||||
TransactionalEventListener annotation = ApplicationListenerMethodTransactionalAdapter.findAnnotation(method);
|
||||
assertEquals("Wrong phase for '" + method + "'", expected, annotation.phase());
|
||||
}
|
||||
|
||||
private void supportsEventType(boolean match, Method method, ResolvableType eventType) {
|
||||
ApplicationListenerMethodAdapter adapter = createTestInstance(method);
|
||||
assertEquals("Wrong match for event '" + eventType + "' on " + method,
|
||||
match, adapter.supportsEventType(eventType));
|
||||
}
|
||||
|
||||
static class PhaseConfigurationTestListener {
|
||||
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 {
|
||||
|
||||
public void noAnnotation(String data) {
|
||||
}
|
||||
@@ -76,6 +109,15 @@ public class ApplicationListenerMethodTransactionalAdapterTests {
|
||||
public void phaseSet(String data) {
|
||||
}
|
||||
|
||||
@TransactionalEventListener(classes = {String.class, Integer.class},
|
||||
phase = TransactionPhase.AFTER_COMPLETION)
|
||||
public void phaseAndClassesSet() {
|
||||
}
|
||||
|
||||
@TransactionalEventListener(String.class)
|
||||
public void valueSet() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7900,6 +7900,19 @@ As you can see above, the method signature actually _infer_ which even type it l
|
||||
also works for nested generics as long as the actual event resolves the generics parameter you
|
||||
would filter on.
|
||||
|
||||
If your method should listen to several events or if you want to define it with no
|
||||
parameter at all, the event type(s) can also be specified on the annotation itself:
|
||||
|
||||
[source,java,indent=0]
|
||||
[subs="verbatim,quotes"]
|
||||
----
|
||||
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
|
||||
public void handleContextStart() {
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
It is also possible to add additional runtime filtering via the `condition` attribute of the
|
||||
annotation that defines a <<expressions,`SpEL` expression>> that should match to actually invoke
|
||||
the method for a particular event.
|
||||
|
||||
Reference in New Issue
Block a user