Browse Source

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
pull/835/head
Stephane Nicoll 9 years ago
parent
commit
bf786c3176
  1. 94
      spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java
  2. 23
      spring-context/src/main/java/org/springframework/context/event/EventListener.java
  3. 99
      spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java
  4. 6
      spring-tx/src/main/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapter.java
  5. 16
      spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java
  6. 50
      spring-tx/src/test/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapterTests.java
  7. 13
      src/asciidoc/core-beans.adoc

94
spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java

@ -20,7 +20,10 @@ import java.lang.annotation.Annotation; @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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
spring-context/src/main/java/org/springframework/context/event/EventListener.java

@ -23,12 +23,14 @@ import java.lang.annotation.RetentionPolicy; @@ -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; @@ -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.

99
spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java

@ -113,6 +113,36 @@ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEv @@ -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 @@ -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 @@ -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 @@ -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 @@ -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())) {

6
spring-tx/src/main/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapter.java

@ -25,7 +25,7 @@ import org.springframework.context.ApplicationEvent; @@ -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 @@ -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 + "'");
}

16
spring-tx/src/main/java/org/springframework/transaction/event/TransactionalEventListener.java

@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy; @@ -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 { @@ -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.

50
spring-tx/src/test/java/org/springframework/transaction/event/ApplicationListenerMethodTransactionalAdapterTests.java

@ -22,6 +22,9 @@ import org.junit.Rule; @@ -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 { @@ -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 { @@ -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 { @@ -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() {
}
}
}

13
src/asciidoc/core-beans.adoc

@ -7900,6 +7900,19 @@ As you can see above, the method signature actually _infer_ which even type it l @@ -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.

Loading…
Cancel
Save