From bf786c31760722cfa9119b7c0b5412e9c1b7bd5d Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 8 Jul 2015 14:51:07 +0200 Subject: [PATCH] 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 --- .../ApplicationListenerMethodAdapter.java | 94 ++++++++++++++---- .../context/event/EventListener.java | 23 ++++- ...ApplicationListenerMethodAdapterTests.java | 99 ++++++++++++++++++- ...ionListenerMethodTransactionalAdapter.java | 6 +- .../event/TransactionalEventListener.java | 16 +++ ...stenerMethodTransactionalAdapterTests.java | 50 +++++++++- src/asciidoc/core-beans.adoc | 13 +++ 7 files changed, 268 insertions(+), 33 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index b113df69dd..491022c613 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -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 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; - } - else if (PayloadApplicationEvent.class.isAssignableFrom(eventType.getRawClass())) { - ResolvableType payloadType = eventType.as(PayloadApplicationEvent.class).getGeneric(); - return eventType.hasUnresolvableGenerics() || this.declaredEventType.isAssignableFrom(payloadType); + 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; + } + } } - 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. *

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 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 types = new ArrayList(); + 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 diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListener.java b/spring-context/src/main/java/org/springframework/context/event/EventListener.java index 88f78c11ce..a69ca4d8af 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListener.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventListener.java @@ -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. * *

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. diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java index b1643b12c8..b9cc4dd0d8 100644 --- a/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java @@ -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 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 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 event = new PayloadApplicationEvent<>(this, "test"); + invokeListener(method, event); + verify(this.sampleEvents, times(1)).handleStringOrInteger(); + PayloadApplicationEvent event2 = new PayloadApplicationEvent<>(this, 123); + invokeListener(method, event2); + verify(this.sampleEvents, times(2)).handleStringOrInteger(); + PayloadApplicationEvent 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 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 event) { if ("fail".equals(event.getPayload())) { diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapter.java b/spring-tx/src/main/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapter.java index 7934a6eba1..c17373367b 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapter.java +++ b/spring-tx/src/main/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapter.java @@ -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 + "'"); } diff --git a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java index 49b1744718..b47d26fe66 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java +++ b/spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java @@ -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. diff --git a/spring-tx/src/test/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapterTests.java b/spring-tx/src/test/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapterTests.java index 5260ad6eac..901398f3d4 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapterTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapterTests.java @@ -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)); + } + + 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 PhaseConfigurationTestListener { + 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() { + } + } } diff --git a/src/asciidoc/core-beans.adoc b/src/asciidoc/core-beans.adoc index b73d0398fc..91eae0b796 100644 --- a/src/asciidoc/core-beans.adoc +++ b/src/asciidoc/core-beans.adoc @@ -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 <> that should match to actually invoke the method for a particular event.