From f0fca890bbc5029ed1a322778134f229da23b2b7 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 23 Jan 2015 11:57:58 +0100 Subject: [PATCH] Annotation-based event listeners Add support for annotation-based event listeners. Enabled automatically when using Java configuration or can be enabled explicitly via the regular XML element. Detect methods of managed beans annotated with @EventListener, either directly or through a meta-annotation. Annotated methods must define the event type they listen to as a single parameter argument. Events are automatically filtered out according to the method signature. When additional runtime filtering is required, one can specify the `condition` attribute of the annotation that defines a SpEL expression that should match to actually invoke the method for a particular event. The root context exposes the actual `event` (`#root.event`) and method arguments (`#root.args`). Individual method arguments are also exposed via either the `a` or `p` alias (`#a0` refers to the first method argument). Finally, methods arguments are exposed via their names if that information can be discovered. Events can be either an ApplicationEvent or any arbitrary payload. Such payload is wrapped automatically in a PayloadApplicationEvent and managed explicitly internally. As a result, users can now publish and listen for arbitrary objects. If an annotated method has a return value, an non null result is actually published as a new event, something like: @EventListener public FooEvent handle(BarEvent event) { ... } Events can be handled in an aynchronous manner by adding `@Async` to the event method declaration and enabling such infrastructure. Events can also be ordered by adding an `@Order` annotation to the event method. Issue: SPR-11622 --- .../interceptor/CacheEvaluationContext.java | 74 +-- .../interceptor/ExpressionEvaluator.java | 17 +- .../context/ApplicationEventPublisher.java | 11 + .../context/PayloadApplicationEvent.java | 50 ++ .../annotation/AnnotationConfigUtils.java | 15 +- .../ApplicationListenerMethodAdapter.java | 268 ++++++++ .../event/EventExpressionEvaluator.java | 85 +++ .../event/EventExpressionRootObject.java | 46 ++ .../context/event/EventListener.java | 63 ++ .../event/EventListenerMethodProcessor.java | 149 +++++ .../MethodBasedEvaluationContext.java | 96 +++ .../support/AbstractApplicationContext.java | 20 +- .../ClassPathBeanDefinitionScannerTests.java | 34 +- ...AbstractApplicationEventListenerTests.java | 50 +- .../AnnotationDrivenEventListenerTests.java | 619 ++++++++++++++++++ .../event/ApplicationContextEventTests.java | 6 +- ...ApplicationListenerMethodAdapterTests.java | 345 ++++++++++ .../EventPublicationInterceptorTests.java | 12 +- ...enericApplicationListenerAdapterTests.java | 10 +- .../event/test/AbstractIdentifiable.java | 52 ++ .../context/event/test/AnotherTestEvent.java | 32 + .../context/event/test/EventCollector.java | 104 +++ .../context/event/test/Identifiable.java | 31 + .../test/IdentifiableApplicationEvent.java | 67 ++ .../context/event/test/TestEvent.java | 45 ++ .../MethodBasedEvaluationContextTest.java | 76 +++ .../event/simple-event-configuration.xml | 17 + .../broker/BrokerMessageHandlerTests.java | 7 +- ...erRelayMessageHandlerIntegrationTests.java | 7 +- .../setup/StubWebApplicationContext.java | 6 +- .../StompSubProtocolHandlerTests.java | 8 +- 31 files changed, 2288 insertions(+), 134 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java create mode 100644 spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java create mode 100644 spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java create mode 100644 spring-context/src/main/java/org/springframework/context/event/EventExpressionRootObject.java create mode 100644 spring-context/src/main/java/org/springframework/context/event/EventListener.java create mode 100644 spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java create mode 100644 spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java create mode 100644 spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java create mode 100644 spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java create mode 100644 spring-context/src/test/java/org/springframework/context/event/test/AbstractIdentifiable.java create mode 100644 spring-context/src/test/java/org/springframework/context/event/test/AnotherTestEvent.java create mode 100644 spring-context/src/test/java/org/springframework/context/event/test/EventCollector.java create mode 100644 spring-context/src/test/java/org/springframework/context/event/test/Identifiable.java create mode 100644 spring-context/src/test/java/org/springframework/context/event/test/IdentifiableApplicationEvent.java create mode 100644 spring-context/src/test/java/org/springframework/context/event/test/TestEvent.java create mode 100644 spring-context/src/test/java/org/springframework/context/expression/MethodBasedEvaluationContextTest.java create mode 100644 spring-context/src/test/resources/org/springframework/context/event/simple-event-configuration.xml diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java index 1720924b96..8d65e15eae 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java @@ -19,13 +19,9 @@ package org.springframework.cache.interceptor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import org.springframework.aop.support.AopUtils; -import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.expression.MethodBasedEvaluationContext; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.util.ObjectUtils; /** * Cache specific evaluation context that adds a method parameters as SpEL @@ -44,32 +40,14 @@ import org.springframework.util.ObjectUtils; * @author Stephane Nicoll * @since 3.1 */ -class CacheEvaluationContext extends StandardEvaluationContext { - - private final ParameterNameDiscoverer paramDiscoverer; - - private final Method method; - - private final Object[] args; - - private final Class targetClass; - - private final Map methodCache; +class CacheEvaluationContext extends MethodBasedEvaluationContext { private final List unavailableVariables; - private boolean paramLoaded = false; + CacheEvaluationContext(Object rootObject, Method method, Object[] args, + ParameterNameDiscoverer paramDiscoverer) { - - CacheEvaluationContext(Object rootObject, ParameterNameDiscoverer paramDiscoverer, Method method, - Object[] args, Class targetClass, Map methodCache) { - super(rootObject); - - this.paramDiscoverer = paramDiscoverer; - this.method = method; - this.args = args; - this.targetClass = targetClass; - this.methodCache = methodCache; + super(rootObject, method, args, paramDiscoverer); this.unavailableVariables = new ArrayList(); } @@ -93,47 +71,7 @@ class CacheEvaluationContext extends StandardEvaluationContext { if (this.unavailableVariables.contains(name)) { throw new VariableNotAvailableException(name); } - Object variable = super.lookupVariable(name); - if (variable != null) { - return variable; - } - if (!this.paramLoaded) { - loadArgsAsVariables(); - this.paramLoaded = true; - variable = super.lookupVariable(name); - } - return variable; - } - - private void loadArgsAsVariables() { - // shortcut if no args need to be loaded - if (ObjectUtils.isEmpty(this.args)) { - return; - } - - AnnotatedElementKey methodKey = new AnnotatedElementKey(this.method, this.targetClass); - Method targetMethod = this.methodCache.get(methodKey); - if (targetMethod == null) { - targetMethod = AopUtils.getMostSpecificMethod(this.method, this.targetClass); - if (targetMethod == null) { - targetMethod = this.method; - } - this.methodCache.put(methodKey, targetMethod); - } - - // save arguments as indexed variables - for (int i = 0; i < this.args.length; i++) { - setVariable("a" + i, this.args[i]); - setVariable("p" + i, this.args[i]); - } - - String[] parameterNames = this.paramDiscoverer.getParameterNames(targetMethod); - // save parameter names (if discovered) - if (parameterNames != null) { - for (int i = 0; i < parameterNames.length; i++) { - setVariable(parameterNames[i], this.args[i]); - } - } + return super.lookupVariable(name); } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/ExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/cache/interceptor/ExpressionEvaluator.java index b7a75658ef..2c54521a3a 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/ExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/ExpressionEvaluator.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.springframework.aop.support.AopUtils; import org.springframework.cache.Cache; import org.springframework.context.expression.AnnotatedElementKey; import org.springframework.context.expression.CachedExpressionEvaluator; @@ -98,8 +99,9 @@ class ExpressionEvaluator extends CachedExpressionEvaluator { CacheExpressionRootObject rootObject = new CacheExpressionRootObject(caches, method, args, target, targetClass); + Method targetMethod = getTargetMethod(targetClass, method); CacheEvaluationContext evaluationContext = new CacheEvaluationContext(rootObject, - this.paramNameDiscoverer, method, args, targetClass, this.targetMethodCache); + targetMethod, args, this.paramNameDiscoverer); if (result == RESULT_UNAVAILABLE) { evaluationContext.addUnavailableVariable(RESULT_VARIABLE); } @@ -121,5 +123,18 @@ class ExpressionEvaluator extends CachedExpressionEvaluator { return getExpression(this.unlessCache, methodKey, unlessExpression).getValue(evalContext, boolean.class); } + private Method getTargetMethod(Class targetClass, Method method) { + AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass); + Method targetMethod = this.targetMethodCache.get(methodKey); + if (targetMethod == null) { + targetMethod = AopUtils.getMostSpecificMethod(method, targetClass); + if (targetMethod == null) { + targetMethod = method; + } + this.targetMethodCache.put(methodKey, targetMethod); + } + return targetMethod; + } + } diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java index b314cf5aa2..025257fb5d 100644 --- a/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java +++ b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java @@ -21,6 +21,7 @@ package org.springframework.context; * Serves as super-interface for ApplicationContext. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 1.1.1 * @see ApplicationContext * @see ApplicationEventPublisherAware @@ -38,4 +39,14 @@ public interface ApplicationEventPublisher { */ void publishEvent(ApplicationEvent event); + /** + * Notify all matching listeners registered with this + * application of an event. + *

If the specified {@code event} is not an {@link ApplicationEvent}, it + * is wrapped in a {@code GenericApplicationEvent}. + * @param event the event to publish + * @see PayloadApplicationEvent + */ + void publishEvent(Object event); + } diff --git a/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java b/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java new file mode 100644 index 0000000000..1829286ab0 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context; + +import org.springframework.util.Assert; + +/** + * An {@link ApplicationEvent} that carries an arbitrary payload. + *

+ * Mainly intended for internal use within the framework. + * + * @param the payload type of the event + * @author Stephane Nicoll + * @since 4.2 + */ +@SuppressWarnings("serial") +public class PayloadApplicationEvent + extends ApplicationEvent { + + private final T payload; + + public PayloadApplicationEvent(Object source, T payload) { + super(source); + Assert.notNull(payload, "Payload must not be null"); + this.payload = payload; + } + + /** + * Return the payload of the event. + */ + public T getPayload() { + return payload; + } + +} + diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java index b7fbce7c41..a889a920a7 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.event.EventListenerMethodProcessor; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationAwareOrderComparator; @@ -48,6 +49,7 @@ import org.springframework.util.ClassUtils; * @author Juergen Hoeller * @author Chris Beams * @author Phillip Webb + * @author Stephane Nicoll * @since 2.5 * @see ContextAnnotationAutowireCandidateResolver * @see CommonAnnotationBeanPostProcessor @@ -103,6 +105,11 @@ public class AnnotationConfigUtils { private static final String PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME = "org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"; + /** + * The bean name of the internally managed @EventListener annotation processor. + */ + public static final String EVENT_LISTENER_PROCESSOR_BEAN_NAME = + "org.springframework.context.event.internalEventListenerProcessor"; private static final boolean jsr250Present = ClassUtils.isPresent("javax.annotation.Resource", AnnotationConfigUtils.class.getClassLoader()); @@ -183,6 +190,12 @@ public class AnnotationConfigUtils { beanDefs.add(registerPostProcessor(registry, def, PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)); } + if (!registry.containsBeanDefinition(EVENT_LISTENER_PROCESSOR_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(EventListenerMethodProcessor.class); + def.setSource(source); + beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_PROCESSOR_BEAN_NAME)); + } + return beanDefs; } 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 new file mode 100644 index 0000000000..cb40c2d608 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -0,0 +1,268 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.UndeclaredThrowableException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.PayloadApplicationEvent; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.Order; +import org.springframework.expression.EvaluationContext; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link GenericApplicationListener} adapter that delegates the processing of + * an event to an {@link EventListener} annotated method. + * + *

Unwrap the content of a {@link PayloadApplicationEvent} if necessary + * to allow method declaration to define any arbitrary event type. + * + *

If a condition is defined, it is evaluated prior to invoking the + * underlying method. + * + * @author Stephane Nicoll + * @since 4.2 + */ +public class ApplicationListenerMethodAdapter implements GenericApplicationListener { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final String beanName; + + private final Method method; + + private final Class targetClass; + + private final Method bridgedMethod; + + private final ResolvableType declaredEventType; + + private final AnnotatedElementKey methodKey; + + private ApplicationContext applicationContext; + + private EventExpressionEvaluator evaluator; + + 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.methodKey = new AnnotatedElementKey(this.method, this.targetClass); + } + + /** + * Initialize this instance. + */ + void init(ApplicationContext applicationContext, EventExpressionEvaluator evaluator) { + this.applicationContext = applicationContext; + this.evaluator = evaluator; + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + Object[] args = resolveArguments(event); + if (shouldHandle(event, args)) { + Object result = doInvoke(args); + if (result != null) { + handleResult(result); + } + else { + logger.trace("No result object given - no result to handle"); + } + } + } + + /** + * Resolve the method arguments to use for the specified {@link ApplicationEvent}. + *

These arguments will be used to invoke the method handled by this instance. Can + * return {@code null} to indicate that no suitable arguments could be resolved and + * 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()) + && event instanceof PayloadApplicationEvent) { + Object payload = ((PayloadApplicationEvent) event).getPayload(); + if (this.declaredEventType.isAssignableFrom(ResolvableType.forClass(payload.getClass()))) { + return new Object[] {payload}; + } + } + else { + return new Object[] {event}; + } + return null; + } + + protected void handleResult(Object result) { + Assert.notNull(this.applicationContext, "ApplicationContext must no be null."); + this.applicationContext.publishEvent(result); + } + + + private boolean shouldHandle(ApplicationEvent event, Object[] args) { + if (args == null) { + return false; + } + EventListener eventListener = AnnotationUtils.findAnnotation(this.method, EventListener.class); + String condition = (eventListener != null ? eventListener.condition() : null); + if (StringUtils.hasText(condition)) { + Assert.notNull(this.evaluator, "Evaluator must no be null."); + EvaluationContext evaluationContext = this.evaluator.createEvaluationContext(event, + this.targetClass, this.method, args); + return this.evaluator.condition(condition, this.methodKey, evaluationContext); + } + return true; + } + + @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); + } + return false; + } + + @Override + public boolean supportsSourceType(Class sourceType) { + return true; + } + + @Override + public int getOrder() { + Order order = AnnotationUtils.findAnnotation(this.method, Order.class); + return (order != null ? order.value() : 0); + } + + /** + * Invoke the event listener method with the given argument values. + */ + protected Object doInvoke(Object... args) { + Object bean = getTargetBean(); + ReflectionUtils.makeAccessible(this.bridgedMethod); + try { + return this.bridgedMethod.invoke(bean, args); + } + catch (IllegalArgumentException ex) { + assertTargetBean(this.bridgedMethod, bean, args); + throw new IllegalStateException(getInvocationErrorMessage(bean, ex.getMessage(), args), ex); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException(getInvocationErrorMessage(bean, ex.getMessage(), args), ex); + } + catch (InvocationTargetException ex) { + // Throw underlying exception + Throwable targetException = ex.getTargetException(); + if (targetException instanceof RuntimeException) { + throw (RuntimeException) targetException; + } + else { + String msg = getInvocationErrorMessage(bean, "Failed to invoke event listener method", args); + throw new UndeclaredThrowableException(targetException, msg); + } + } + } + + /** + * Return the target bean instance to use. + */ + protected Object getTargetBean() { + Assert.notNull(this.applicationContext, "ApplicationContext must no be null."); + return this.applicationContext.getBean(this.beanName); + } + + /** + * Add additional details such as the bean type and method signature to + * the given error message. + * @param message error message to append the HandlerMethod details to + */ + protected String getDetailedErrorMessage(Object bean, String message) { + StringBuilder sb = new StringBuilder(message).append("\n"); + sb.append("HandlerMethod details: \n"); + sb.append("Bean [").append(bean.getClass().getName()).append("]\n"); + sb.append("Method [").append(this.bridgedMethod.toGenericString()).append("]\n"); + return sb.toString(); + } + + /** + * Assert that the target bean class is an instance of the class where the given + * method is declared. In some cases the actual bean instance at event- + * processing time may be a JDK dynamic proxy (lazy initialization, prototype + * beans, and others). Event listener beans that require proxying should prefer + * class-based proxy mechanisms. + */ + private void assertTargetBean(Method method, Object targetBean, Object[] args) { + Class methodDeclaringClass = method.getDeclaringClass(); + Class targetBeanClass = targetBean.getClass(); + if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) { + String msg = "The event listener method class '" + methodDeclaringClass.getName() + + "' is not an instance of the actual bean instance '" + + targetBeanClass.getName() + "'. If the bean requires proxying " + + "(e.g. due to @Transactional), please use class-based proxying."; + throw new IllegalStateException(getInvocationErrorMessage(targetBean, msg, args)); + } + } + + private String getInvocationErrorMessage(Object bean, String message, Object[] resolvedArgs) { + StringBuilder sb = new StringBuilder(getDetailedErrorMessage(bean, message)); + sb.append("Resolved arguments: \n"); + for (int i = 0; i < resolvedArgs.length; i++) { + sb.append("[").append(i).append("] "); + if (resolvedArgs[i] == null) { + sb.append("[null] \n"); + } + else { + sb.append("[type=").append(resolvedArgs[i].getClass().getName()).append("] "); + sb.append("[value=").append(resolvedArgs[i]).append("]\n"); + } + } + return sb.toString(); + } + + + private ResolvableType resolveDeclaredEventType() { + Parameter[] parameters = this.method.getParameters(); + if (parameters.length != 1) { + throw new IllegalStateException("Only one parameter is allowed " + + "for event listener method: " + method); + } + return ResolvableType.forMethodParameter(this.method, 0); + } + + @Override + public String toString() { + return this.method.toGenericString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java new file mode 100644 index 0000000000..9bf4fcf6e4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.aop.support.AopUtils; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.expression.AnnotatedElementKey; +import org.springframework.context.expression.CachedExpressionEvaluator; +import org.springframework.context.expression.MethodBasedEvaluationContext; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; + +/** + * Utility class handling the SpEL expression parsing. Meant to be used + * as a reusable, thread-safe component. + * + * @author Stephane Nicoll + * @since 4.2 + * @see CachedExpressionEvaluator + */ +class EventExpressionEvaluator extends CachedExpressionEvaluator { + + // shared param discoverer since it caches data internally + private final ParameterNameDiscoverer paramNameDiscoverer = new DefaultParameterNameDiscoverer(); + + private final Map conditionCache = new ConcurrentHashMap(64); + + private final Map targetMethodCache = new ConcurrentHashMap(64); + + /** + * Create the suitable {@link EvaluationContext} for the specified event handling + * on the specified method. + */ + public EvaluationContext createEvaluationContext(ApplicationEvent event, Class targetClass, + Method method, Object[] args) { + + Method targetMethod = getTargetMethod(targetClass, method); + EventExpressionRootObject root = new EventExpressionRootObject(event, args); + return new MethodBasedEvaluationContext(root, targetMethod, args, this.paramNameDiscoverer); + } + + /** + * Specify if the condition defined by the specified expression matches. + */ + public boolean condition(String conditionExpression, + AnnotatedElementKey elementKey, EvaluationContext evalContext) { + + return getExpression(this.conditionCache, elementKey, conditionExpression) + .getValue(evalContext, boolean.class); + } + + private Method getTargetMethod(Class targetClass, Method method) { + AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass); + Method targetMethod = this.targetMethodCache.get(methodKey); + if (targetMethod == null) { + targetMethod = AopUtils.getMostSpecificMethod(method, targetClass); + if (targetMethod == null) { + targetMethod = method; + } + this.targetMethodCache.put(methodKey, targetMethod); + } + return targetMethod; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/EventExpressionRootObject.java b/spring-context/src/main/java/org/springframework/context/event/EventExpressionRootObject.java new file mode 100644 index 0000000000..2b06fdb4bf --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/EventExpressionRootObject.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event; + +import org.springframework.context.ApplicationEvent; + +/** + * Root object used during event listener expression evaluation. + * + * @author Stephane Nicoll + * @since 4.2 + */ +class EventExpressionRootObject { + + private final ApplicationEvent event; + + private final Object[] args; + + public EventExpressionRootObject(ApplicationEvent event, Object[] args) { + this.event = event; + this.args = args; + } + + public ApplicationEvent getEvent() { + return event; + } + + public Object[] getArgs() { + return args; + } + +} 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 new file mode 100644 index 0000000000..85c3e98823 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/EventListener.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.ApplicationEvent; + +/** + * 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. + * + *

Processing of {@code @EventListener} annotations is performed via + * {@link EventListenerMethodProcessor} that is registered automatically + * when using Java config or via the {@code } + * XML element. + * + *

Annotated methods may have a non-{@code void} return type. When they + * do, the result of the method invocation is sent as a new event. It is + * also possible to defined the order in which listeners for a certain + * event are invoked. To do so, add a regular {code @Order} annotation + * alongside this annotation. + * + *

While it is possible to define any arbitrary exception types, checked + * exceptions will be wrapped in a {@link java.lang.reflect.UndeclaredThrowableException} + * as the caller only handles runtime exceptions. + * + * @author Stephane Nicoll + * @since 4.2 + * @see EventListenerMethodProcessor + */ +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface EventListener { + + /** + * Spring Expression Language (SpEL) attribute used for conditioning the event handling. + *

Default is "", meaning the event is always handled. + */ + String condition() default ""; + +} \ No newline at end of file diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java new file mode 100644 index 0000000000..8d2bb3e212 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.SpringProxy; +import org.springframework.aop.scope.ScopedProxyUtils; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * Register {@link EventListener} annotated method as individual {@link ApplicationListener} + * instances. + * + * @author Stephane Nicoll + * @since 4.2 + */ +public class EventListenerMethodProcessor implements SmartInitializingSingleton, ApplicationContextAware { + + protected final Log logger = LogFactory.getLog(getClass()); + + private ConfigurableApplicationContext applicationContext; + + private final EventExpressionEvaluator evaluator = new EventExpressionEvaluator(); + + private final Set> nonAnnotatedClasses = + Collections.newSetFromMap(new ConcurrentHashMap, Boolean>(64)); + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + Assert.isTrue(applicationContext instanceof ConfigurableApplicationContext, + "ApplicationContext does not implement ConfigurableApplicationContext"); + this.applicationContext = (ConfigurableApplicationContext) applicationContext; + + } + + @Override + public void afterSingletonsInstantiated() { + String[] allBeanNames = this.applicationContext.getBeanNamesForType(Object.class); + for (String beanName : allBeanNames) { + if (!ScopedProxyUtils.isScopedTarget(beanName)) { + Class type = this.applicationContext.getType(beanName); + try { + processBean(beanName, type); + } + catch (RuntimeException e) { + throw new BeanInitializationException("Failed to process @EventListener " + + "annotation on bean with name '" + beanName + "'", e); + } + } + } + } + + protected void processBean(String beanName, final Class type) { + Class targetType = getTargetClass(beanName, type); + if (!this.nonAnnotatedClasses.contains(targetType)) { + final Set annotatedMethods = new LinkedHashSet(1); + Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(targetType); + for (Method method : methods) { + EventListener eventListener = AnnotationUtils.findAnnotation(method, EventListener.class); + if (eventListener != null) { + if (!type.equals(targetType)) { + method = getProxyMethod(type, method); + } + ApplicationListenerMethodAdapter applicationListener = + new ApplicationListenerMethodAdapter(beanName, type, method); + applicationListener.init(this.applicationContext, this.evaluator); + this.applicationContext.addApplicationListener(applicationListener); + annotatedMethods.add(method); + } + } + if (annotatedMethods.isEmpty()) { + this.nonAnnotatedClasses.add(type); + if (logger.isDebugEnabled()) { + logger.debug("No @EventListener annotations found on bean class: " + type); + } + } + else { + // Non-empty set of methods + if (logger.isDebugEnabled()) { + logger.debug(annotatedMethods.size() + " @EventListener methods processed on bean '" + beanName + + "': " + annotatedMethods); + } + } + } + } + + private Class getTargetClass(String beanName, Class type) { + if (SpringProxy.class.isAssignableFrom(type)) { + Object bean = this.applicationContext.getBean(beanName); + return AopUtils.getTargetClass(bean); + } + else { + return type; + } + } + + private Method getProxyMethod(Class proxyType, Method method) { + try { + // Found a @EventListener method on the target class for this JDK proxy -> + // is it also present on the proxy itself? + return proxyType.getMethod(method.getName(), method.getParameterTypes()); + } + catch (SecurityException ex) { + ReflectionUtils.handleReflectionException(ex); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException(String.format( + "@EventListener method '%s' found on bean target class '%s', " + + "but not found in any interface(s) for bean JDK proxy. Either " + + "pull the method up to an interface or switch to subclass (CGLIB) " + + "proxies by setting proxy-target-class/proxyTargetClass " + + "attribute to 'true'", method.getName(), method.getDeclaringClass().getSimpleName())); + } + return null; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java b/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java new file mode 100644 index 0000000000..d5c1e21245 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.expression; + +import java.lang.reflect.Method; + +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.util.ObjectUtils; + +/** + * A method-based {@link org.springframework.expression.EvaluationContext} that + * provides explicit support for method-based invocations. + *

+ * Expose the actual method arguments using the following aliases: + *

    + *
  1. pX where X is the index of the argument (p0 for the first argument)
  2. + *
  3. aX where X is the index of the argument (a1 for the second argument)
  4. + *
  5. the name of the parameter as discovered by a configurable {@link ParameterNameDiscoverer}
  6. + *
+ * + * @author Stephane Nicoll + * @since 4.2.0 + */ +public class MethodBasedEvaluationContext extends StandardEvaluationContext { + + private final Method method; + + private final Object[] args; + + private final ParameterNameDiscoverer paramDiscoverer; + + private boolean paramLoaded = false; + + public MethodBasedEvaluationContext(Object rootObject, Method method, Object[] args, + ParameterNameDiscoverer paramDiscoverer) { + + super(rootObject); + this.method = method; + this.args = args; + this.paramDiscoverer = paramDiscoverer; + } + + @Override + public Object lookupVariable(String name) { + Object variable = super.lookupVariable(name); + if (variable != null) { + return variable; + } + if (!this.paramLoaded) { + lazyLoadArguments(); + this.paramLoaded = true; + variable = super.lookupVariable(name); + } + return variable; + } + + /** + * Load the param information only when needed. + */ + protected void lazyLoadArguments() { + // shortcut if no args need to be loaded + if (ObjectUtils.isEmpty(this.args)) { + return; + } + + // save arguments as indexed variables + for (int i = 0; i < this.args.length; i++) { + setVariable("a" + i, this.args[i]); + setVariable("p" + i, this.args[i]); + } + + String[] parameterNames = this.paramDiscoverer.getParameterNames(this.method); + // save parameter names (if discovered) + if (parameterNames != null) { + for (int i = 0; i < parameterNames.length; i++) { + setVariable(parameterNames[i], this.args[i]); + } + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index aac94c976d..8b5ba17202 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -47,6 +47,7 @@ import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.EnvironmentAware; +import org.springframework.context.PayloadApplicationEvent; import org.springframework.context.HierarchicalMessageSource; import org.springframework.context.LifecycleProcessor; import org.springframework.context.MessageSource; @@ -329,12 +330,27 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader publishEvent(event, null); } - protected void publishEvent(ApplicationEvent event, ResolvableType eventType) { + @Override + public void publishEvent(Object event) { + publishEvent(event, null); + } + + protected void publishEvent(Object event, ResolvableType eventType) { Assert.notNull(event, "Event must not be null"); if (logger.isTraceEnabled()) { logger.trace("Publishing event in " + getDisplayName() + ": " + event); } - getApplicationEventMulticaster().multicastEvent(event, eventType); + final ApplicationEvent applicationEvent; + if (event instanceof ApplicationEvent) { + applicationEvent = (ApplicationEvent) event; + } + else { + applicationEvent = new PayloadApplicationEvent(this, event); + if (eventType == null) { + eventType = ResolvableType.forClassWithGenerics(PayloadApplicationEvent.class, event.getClass()); + } + } + getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType); if (this.parent != null) { if (this.parent instanceof AbstractApplicationContext) { ((AbstractApplicationContext) this.parent).publishEvent(event, eventType); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java index 11a08cb5de..a125a27e66 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathBeanDefinitionScannerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -55,7 +55,7 @@ public class ClassPathBeanDefinitionScannerTests { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(10, beanCount); + assertEquals(11, beanCount); assertTrue(context.containsBean("serviceInvocationCounter")); assertTrue(context.containsBean("fooServiceImpl")); assertTrue(context.containsBean("stubFooDao")); @@ -66,6 +66,7 @@ public class ClassPathBeanDefinitionScannerTests { assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); context.refresh(); FooServiceImpl service = context.getBean("fooServiceImpl", FooServiceImpl.class); assertTrue(context.getDefaultListableBeanFactory().containsSingleton("myNamedComponent")); @@ -98,7 +99,7 @@ public class ClassPathBeanDefinitionScannerTests { GenericApplicationContext context = new GenericApplicationContext(); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(10, beanCount); + assertEquals(11, beanCount); scanner.scan(BASE_PACKAGE); assertTrue(context.containsBean("serviceInvocationCounter")); assertTrue(context.containsBean("fooServiceImpl")); @@ -218,11 +219,12 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, false); scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(5, beanCount); + assertEquals(6, beanCount); assertTrue(context.containsBean("messageBean")); assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); } @Test @@ -231,7 +233,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, false); scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(5, beanCount); + assertEquals(6, beanCount); assertTrue(context.containsBean("messageBean")); assertFalse(context.containsBean("serviceInvocationCounter")); assertFalse(context.containsBean("fooServiceImpl")); @@ -241,6 +243,7 @@ public class ClassPathBeanDefinitionScannerTests { assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); } @Test @@ -249,7 +252,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addIncludeFilter(new AnnotationTypeFilter(CustomComponent.class)); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(11, beanCount); + assertEquals(12, beanCount); assertTrue(context.containsBean("messageBean")); assertTrue(context.containsBean("serviceInvocationCounter")); assertTrue(context.containsBean("fooServiceImpl")); @@ -259,6 +262,7 @@ public class ClassPathBeanDefinitionScannerTests { assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); } @Test @@ -267,7 +271,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addExcludeFilter(new AnnotationTypeFilter(Aspect.class)); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(9, beanCount); + assertEquals(10, beanCount); assertFalse(context.containsBean("serviceInvocationCounter")); assertTrue(context.containsBean("fooServiceImpl")); assertTrue(context.containsBean("stubFooDao")); @@ -276,6 +280,7 @@ public class ClassPathBeanDefinitionScannerTests { assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); } @Test @@ -284,7 +289,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true); scanner.addExcludeFilter(new AssignableTypeFilter(FooService.class)); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(9, beanCount); + assertEquals(10, beanCount); assertFalse(context.containsBean("fooServiceImpl")); assertTrue(context.containsBean("serviceInvocationCounter")); assertTrue(context.containsBean("stubFooDao")); @@ -293,6 +298,7 @@ public class ClassPathBeanDefinitionScannerTests { assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); } @Test @@ -320,7 +326,7 @@ public class ClassPathBeanDefinitionScannerTests { scanner.addExcludeFilter(new AssignableTypeFilter(FooService.class)); scanner.addExcludeFilter(new AnnotationTypeFilter(Aspect.class)); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(8, beanCount); + assertEquals(9, beanCount); assertFalse(context.containsBean("fooServiceImpl")); assertFalse(context.containsBean("serviceInvocationCounter")); assertTrue(context.containsBean("stubFooDao")); @@ -329,6 +335,7 @@ public class ClassPathBeanDefinitionScannerTests { assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); } @Test @@ -337,7 +344,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setBeanNameGenerator(new TestBeanNameGenerator()); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(10, beanCount); + assertEquals(11, beanCount); assertFalse(context.containsBean("fooServiceImpl")); assertTrue(context.containsBean("fooService")); assertTrue(context.containsBean("serviceInvocationCounter")); @@ -347,6 +354,7 @@ public class ClassPathBeanDefinitionScannerTests { assertTrue(context.containsBean(AnnotationConfigUtils.AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.REQUIRED_ANNOTATION_PROCESSOR_BEAN_NAME)); assertTrue(context.containsBean(AnnotationConfigUtils.COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); + assertTrue(context.containsBean(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)); } @Test @@ -356,7 +364,7 @@ public class ClassPathBeanDefinitionScannerTests { GenericApplicationContext multiPackageContext = new GenericApplicationContext(); ClassPathBeanDefinitionScanner multiPackageScanner = new ClassPathBeanDefinitionScanner(multiPackageContext); int singlePackageBeanCount = singlePackageScanner.scan(BASE_PACKAGE); - assertEquals(10, singlePackageBeanCount); + assertEquals(11, singlePackageBeanCount); multiPackageScanner.scan(BASE_PACKAGE, "org.springframework.dao.annotation"); // assertTrue(multiPackageBeanCount > singlePackageBeanCount); } @@ -367,7 +375,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); int initialBeanCount = context.getBeanDefinitionCount(); int scannedBeanCount = scanner.scan(BASE_PACKAGE); - assertEquals(10, scannedBeanCount); + assertEquals(11, scannedBeanCount); assertEquals(scannedBeanCount, context.getBeanDefinitionCount() - initialBeanCount); int addedBeanCount = scanner.scan("org.springframework.aop.aspectj.annotation"); assertEquals(initialBeanCount + scannedBeanCount + addedBeanCount, context.getBeanDefinitionCount()); @@ -380,7 +388,7 @@ public class ClassPathBeanDefinitionScannerTests { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); scanner.setBeanNameGenerator(new TestBeanNameGenerator()); int beanCount = scanner.scan(BASE_PACKAGE); - assertEquals(10, beanCount); + assertEquals(11, beanCount); context.refresh(); FooServiceImpl fooService = context.getBean("fooService", FooServiceImpl.class); diff --git a/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java index 98dd3ea1ca..7298428f07 100644 --- a/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java @@ -30,19 +30,19 @@ public abstract class AbstractApplicationEventListenerTests { protected ResolvableType getGenericApplicationEventType(String fieldName) { try { - return ResolvableType.forField(GenericApplicationEvents.class.getField(fieldName)); + return ResolvableType.forField(TestEvents.class.getField(fieldName)); } catch (NoSuchFieldException e) { throw new IllegalStateException("No such field on Events '" + fieldName + "'"); } } - protected static class GenericApplicationEvent + protected static class GenericTestEvent extends ApplicationEvent { private final T payload; - public GenericApplicationEvent(Object source, T payload) { + public GenericTestEvent(Object source, T payload) { super(source); this.payload = payload; } @@ -53,76 +53,72 @@ public abstract class AbstractApplicationEventListenerTests { } - protected static class StringEvent extends GenericApplicationEvent { + protected static class StringEvent extends GenericTestEvent { public StringEvent(Object source, String payload) { super(source, payload); } } - protected static class LongEvent extends GenericApplicationEvent { + protected static class LongEvent extends GenericTestEvent { public LongEvent(Object source, Long payload) { super(source, payload); } } - protected GenericApplicationEvent createGenericEvent(T payload) { - return new GenericApplicationEvent<>(this, payload); + protected GenericTestEvent createGenericTestEvent(T payload) { + return new GenericTestEvent<>(this, payload); } - static class GenericEventListener implements ApplicationListener> { + static class GenericEventListener implements ApplicationListener> { @Override - public void onApplicationEvent(GenericApplicationEvent event) { - + public void onApplicationEvent(GenericTestEvent event) { } } - static class ObjectEventListener implements ApplicationListener> { + static class ObjectEventListener implements ApplicationListener> { @Override - public void onApplicationEvent(GenericApplicationEvent event) { - + public void onApplicationEvent(GenericTestEvent event) { } } static class UpperBoundEventListener - implements ApplicationListener> { + implements ApplicationListener> { @Override - public void onApplicationEvent(GenericApplicationEvent event) { - + public void onApplicationEvent(GenericTestEvent event) { } } - static class StringEventListener implements ApplicationListener> { + static class StringEventListener implements ApplicationListener> { @Override - public void onApplicationEvent(GenericApplicationEvent event) { - + public void onApplicationEvent(GenericTestEvent event) { } } static class RawApplicationListener implements ApplicationListener { @Override public void onApplicationEvent(ApplicationEvent event) { - } } - @SuppressWarnings("unused") - static class GenericApplicationEvents { + static class TestEvents { - public GenericApplicationEvent wildcardEvent; + public ApplicationEvent applicationEvent; - public GenericApplicationEvent stringEvent; + public GenericTestEvent wildcardEvent; - public GenericApplicationEvent longEvent; + public GenericTestEvent stringEvent; - public GenericApplicationEvent illegalStateExceptionEvent; + public GenericTestEvent longEvent; - public GenericApplicationEvent ioExceptionEvent; + public GenericTestEvent illegalStateExceptionEvent; + + public GenericTestEvent ioExceptionEvent; } diff --git a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java new file mode 100644 index 0000000000..234fc15b4f --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java @@ -0,0 +1,619 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.PayloadApplicationEvent; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.context.event.test.AbstractIdentifiable; +import org.springframework.context.event.test.AnotherTestEvent; +import org.springframework.context.event.test.EventCollector; +import org.springframework.context.event.test.Identifiable; +import org.springframework.context.event.test.TestEvent; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.core.annotation.Order; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Component; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * @author Stephane Nicoll + */ +public class AnnotationDrivenEventListenerTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private ConfigurableApplicationContext context; + + private EventCollector eventCollector; + + private CountDownLatch countDownLatch; // 1 call by default + + @After + public void closeContext() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void simpleEventJavaConfig() { + load(TestEventListener.class); + TestEvent event = new TestEvent(this, "test"); + TestEventListener listener = this.context.getBean(TestEventListener.class); + this.eventCollector.assertNoEventReceived(listener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void simpleEventXmlConfig() { + this.context = new ClassPathXmlApplicationContext( + "org/springframework/context/event/simple-event-configuration.xml"); + TestEvent event = new TestEvent(this, "test"); + TestEventListener listener = this.context.getBean(TestEventListener.class); + this.eventCollector = getEventCollector(this.context); + + this.eventCollector.assertNoEventReceived(listener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void metaAnnotationIsDiscovered() { + load(MetaAnnotationListenerTestBean.class); + + MetaAnnotationListenerTestBean bean = context.getBean(MetaAnnotationListenerTestBean.class); + this.eventCollector.assertNoEventReceived(bean); + + TestEvent event = new TestEvent(); + this.context.publishEvent(event); + this.eventCollector.assertEvent(bean, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void contextEventsAreReceived() { + load(ContextEventListener.class); + ContextEventListener listener = this.context.getBean(ContextEventListener.class); + + List events = this.eventCollector.getEvents(listener); + assertEquals("Wrong number of initial context events", 1, events.size()); + assertEquals(ContextRefreshedEvent.class, events.get(0).getClass()); + + this.context.stop(); + List eventsAfterStop = this.eventCollector.getEvents(listener); + assertEquals("Wrong number of context events on shutdown", 2, eventsAfterStop.size()); + assertEquals(ContextStoppedEvent.class, eventsAfterStop.get(1).getClass()); + this.eventCollector.assertTotalEventsCount(2); + } + + @Test + public void methodSignatureNoEvent() { + AnnotationConfigApplicationContext failingContext = + new AnnotationConfigApplicationContext(); + failingContext.register(BasicConfiguration.class, + InvalidMethodSignatureEventListener.class); + + thrown.expect(BeanInitializationException.class); + thrown.expectMessage(InvalidMethodSignatureEventListener.class.getName()); + thrown.expectMessage("cannotBeCalled"); + failingContext.refresh(); + } + + @Test + public void simpleReply() { + load(TestEventListener.class, ReplyEventListener.class); + AnotherTestEvent event = new AnotherTestEvent(this, "dummy"); + ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertNoEventReceived(replyEventListener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(replyEventListener, event); + this.eventCollector.assertEvent(listener, new TestEvent(replyEventListener, event.getId(), event.msg)); // reply + this.eventCollector.assertTotalEventsCount(2); + } + + @Test + public void nullReplyIgnored() { + load(TestEventListener.class, ReplyEventListener.class); + AnotherTestEvent event = new AnotherTestEvent(this, null); // No response + ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertNoEventReceived(replyEventListener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(replyEventListener, event); + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void eventListenerWorksWithInterfaceProxy() throws Exception { + load(ProxyTestBean.class); + + SimpleService proxy = this.context.getBean(SimpleService.class); + assertTrue("bean should be a proxy", proxy instanceof Advised); + this.eventCollector.assertNoEventReceived(proxy.getId()); + + TestEvent event = new TestEvent(); + this.context.publishEvent(event); + this.eventCollector.assertEvent(proxy.getId(), event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void methodNotAvailableOnProxyIsDetected() throws Exception { + thrown.expect(BeanInitializationException.class); + thrown.expectMessage("handleIt2"); + load(InvalidProxyTestBean.class); + } + + @Test + public void eventListenerWorksWithCglibProxy() throws Exception { + load(CglibProxyTestBean.class); + + CglibProxyTestBean proxy = this.context.getBean(CglibProxyTestBean.class); + assertTrue("bean should be a cglib proxy", AopUtils.isCglibProxy(proxy)); + this.eventCollector.assertNoEventReceived(proxy.getId()); + + TestEvent event = new TestEvent(); + this.context.publishEvent(event); + this.eventCollector.assertEvent(proxy.getId(), event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void asyncProcessingApplied() throws InterruptedException { + loadAsync(AsyncEventListener.class); + String threadName = Thread.currentThread().getName(); + AnotherTestEvent event = new AnotherTestEvent(this, threadName); + AsyncEventListener listener = this.context.getBean(AsyncEventListener.class); + this.eventCollector.assertNoEventReceived(listener); + + this.context.publishEvent(event); + + countDownLatch.await(2, TimeUnit.SECONDS); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void exceptionPropagated() { + load(ExceptionEventListener.class); + TestEvent event = new TestEvent(this, "fail"); + ExceptionEventListener listener = this.context.getBean(ExceptionEventListener.class); + this.eventCollector.assertNoEventReceived(listener); + try { + this.context.publishEvent(event); + fail("An exception should have thrown"); + } + catch (IllegalStateException e) { + assertEquals("Wrong exception", "Test exception", e.getMessage()); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + } + } + + @Test + public void exceptionNotPropagatedWithAsync() throws InterruptedException { + loadAsync(ExceptionEventListener.class); + AnotherTestEvent event = new AnotherTestEvent(this, "fail"); + ExceptionEventListener listener = this.context.getBean(ExceptionEventListener.class); + this.eventCollector.assertNoEventReceived(listener); + + this.context.publishEvent(event); + countDownLatch.await(2, TimeUnit.SECONDS); + + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void listenerWithSimplePayload() { + load(TestEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.context.publishEvent("test"); + this.eventCollector.assertEvent(listener, "test"); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void listenerWithNonMatchingPayload() { + load(TestEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.context.publishEvent(123L); + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertTotalEventsCount(0); + } + + @Test + public void replyWithPayload() { + load(TestEventListener.class, ReplyEventListener.class); + AnotherTestEvent event = new AnotherTestEvent(this, "String"); + ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); + TestEventListener listener = this.context.getBean(TestEventListener.class); + + + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertNoEventReceived(replyEventListener); + this.context.publishEvent(event); + this.eventCollector.assertEvent(replyEventListener, event); + this.eventCollector.assertEvent(listener, "String"); // reply + this.eventCollector.assertTotalEventsCount(2); + } + + @Test + public void listenerWithGenericApplicationEvent() { + load(GenericEventListener.class); + GenericEventListener listener = this.context.getBean(GenericEventListener.class); + + this.eventCollector.assertNoEventReceived(listener); + this.context.publishEvent("TEST"); + this.eventCollector.assertEvent(listener, "TEST"); + this.eventCollector.assertTotalEventsCount(1); + } + + @Test + public void conditionMatch() { + long timestamp = System.currentTimeMillis(); + load(ConditionalEventListener.class); + TestEvent event = new TestEvent(this, "OK"); + TestEventListener listener = this.context.getBean(ConditionalEventListener.class); + this.eventCollector.assertNoEventReceived(listener); + + this.context.publishEvent(event); + this.eventCollector.assertEvent(listener, event); + this.eventCollector.assertTotalEventsCount(1); + + this.context.publishEvent("OK"); + this.eventCollector.assertEvent(listener, event, "OK"); + this.eventCollector.assertTotalEventsCount(2); + + this.context.publishEvent(timestamp); + this.eventCollector.assertEvent(listener, event, "OK", timestamp); + this.eventCollector.assertTotalEventsCount(3); + } + + @Test + public void conditionDoesNotMatch() { + long maxLong = Long.MAX_VALUE; + load(ConditionalEventListener.class); + TestEvent event = new TestEvent(this, "KO"); + TestEventListener listener = this.context.getBean(ConditionalEventListener.class); + this.eventCollector.assertNoEventReceived(listener); + + this.context.publishEvent(event); + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertTotalEventsCount(0); + + this.context.publishEvent("KO"); + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertTotalEventsCount(0); + + this.context.publishEvent(maxLong); + this.eventCollector.assertNoEventReceived(listener); + this.eventCollector.assertTotalEventsCount(0); + } + + @Test + public void orderedListeners() { + load(OrderedTestListener.class); + OrderedTestListener listener = this.context.getBean(OrderedTestListener.class); + + assertTrue(listener.order.isEmpty()); + this.context.publishEvent("whatever"); + assertThat(listener.order, contains("first", "second", "third")); + } + + private void load(Class... classes) { + List> allClasses = new ArrayList<>(); + allClasses.add(BasicConfiguration.class); + allClasses.addAll(Arrays.asList(classes)); + doLoad(allClasses.toArray(new Class[allClasses.size()])); + } + + private void loadAsync(Class... classes) { + List> allClasses = new ArrayList<>(); + allClasses.add(AsyncConfiguration.class); + allClasses.addAll(Arrays.asList(classes)); + doLoad(allClasses.toArray(new Class[allClasses.size()])); + } + + private void doLoad(Class... classes) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(classes); + this.eventCollector = ctx.getBean(EventCollector.class); + this.countDownLatch = ctx.getBean(CountDownLatch.class); + this.context = ctx; + } + + private EventCollector getEventCollector(ConfigurableApplicationContext context) { + return context.getBean(EventCollector.class); + } + + + @Configuration + static class BasicConfiguration { + + @Bean + public EventCollector eventCollector() { + return new EventCollector(); + } + + @Bean + public CountDownLatch testCountDownLatch() { + return new CountDownLatch(1); + } + + } + + static abstract class AbstractTestEventListener extends AbstractIdentifiable { + + @Autowired + private EventCollector eventCollector; + + protected void collectEvent(Object content) { + this.eventCollector.addEvent(this, content); + } + + } + + @Component + static class TestEventListener extends AbstractTestEventListener { + + @EventListener + public void handle(TestEvent event) { + collectEvent(event); + } + + @EventListener + public void handleString(String content) { + collectEvent(content); + } + + } + + @EventListener + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @interface FooListener { + } + + @Component + static class MetaAnnotationListenerTestBean extends AbstractTestEventListener { + + @FooListener + public void handleIt(TestEvent event) { + collectEvent(event); + } + } + + @Component + static class ContextEventListener extends AbstractTestEventListener { + + @EventListener + public void handleContextEvent(ApplicationContextEvent event) { + collectEvent(event); + } + + } + + @Component + static class InvalidMethodSignatureEventListener { + + @EventListener + public void cannotBeCalled(String s, Integer what) { + } + } + + @Component + static class ReplyEventListener extends AbstractTestEventListener { + + @EventListener + public Object handle(AnotherTestEvent event) { + collectEvent(event); + if (event.msg == null) { + return null; + } + else if (event.msg.equals("String")) { + return event.msg; + } + else { + return new TestEvent(this, event.getId(), event.msg); + } + } + + } + + @Component + static class ExceptionEventListener extends AbstractTestEventListener { + + @Autowired + private CountDownLatch countDownLatch; + + @EventListener + public void handle(TestEvent event) { + collectEvent(event); + if ("fail".equals(event.msg)) { + throw new IllegalStateException("Test exception"); + } + } + + @EventListener + @Async + public void handleAsync(AnotherTestEvent event) { + collectEvent(event); + if ("fail".equals(event.msg)) { + countDownLatch.countDown(); + throw new IllegalStateException("Test exception"); + } + } + } + + @Configuration + @Import(BasicConfiguration.class) + @EnableAsync(proxyTargetClass = true) + static class AsyncConfiguration { + } + + @Component + static class AsyncEventListener extends AbstractTestEventListener { + + @Autowired + private CountDownLatch countDownLatch; + + @EventListener + @Async + public void handleAsync(AnotherTestEvent event) { + assertTrue(!Thread.currentThread().getName().equals(event.msg)); + collectEvent(event); + countDownLatch.countDown(); + } + } + + interface SimpleService extends Identifiable { + + @EventListener + void handleIt(TestEvent event); + + } + + @Component + @Scope(proxyMode = ScopedProxyMode.INTERFACES) + static class ProxyTestBean extends AbstractIdentifiable implements SimpleService { + + @Autowired + private EventCollector eventCollector; + + @Override + public void handleIt(TestEvent event) { + eventCollector.addEvent(this, event); + } + } + + @Component + @Scope(proxyMode = ScopedProxyMode.INTERFACES) + static class InvalidProxyTestBean extends ProxyTestBean { + + @EventListener // does not exist on any interface so it should fail + public void handleIt2(TestEvent event) { + } + } + + @Component + @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS) + static class CglibProxyTestBean extends AbstractTestEventListener { + + @EventListener + public void handleIt(TestEvent event) { + collectEvent(event); + } + } + + @Component + static class GenericEventListener extends AbstractTestEventListener { + + @EventListener + public void handleString(PayloadApplicationEvent event) { + collectEvent(event.getPayload()); + } + } + + @Component + static class ConditionalEventListener extends TestEventListener { + + @EventListener(condition = "'OK'.equals(#root.event.msg)") + @Override + public void handle(TestEvent event) { + super.handle(event); + } + + @Override + @EventListener(condition = "'OK'.equals(#content)") + public void handleString(String content) { + super.handleString(content); + } + + @EventListener(condition = "#root.event.timestamp > #p0") + public void handleTimestamp(Long timestamp) { + collectEvent(timestamp); + } + + } + + @Component + static class OrderedTestListener extends TestEventListener { + + public final List order = new ArrayList<>(); + + @EventListener + @Order(50) + public void handleThird(String payload) { + order.add("third"); + } + + @EventListener + @Order(-50) + public void handleFirst(String payload) { + order.add("first"); + } + + @EventListener + public void handleSecond(String payload) { + order.add("second"); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java index 8c214bb7ca..babe111aa4 100644 --- a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java @@ -58,19 +58,19 @@ public class ApplicationContextEventTests extends AbstractApplicationEventListen @Test public void multicastGenericEvent() { - multicastEvent(true, StringEventListener.class, createGenericEvent("test"), + multicastEvent(true, StringEventListener.class, createGenericTestEvent("test"), getGenericApplicationEventType("stringEvent")); } @Test public void multicastGenericEventWrongType() { - multicastEvent(false, StringEventListener.class, createGenericEvent(123L), + multicastEvent(false, StringEventListener.class, createGenericTestEvent(123L), getGenericApplicationEventType("longEvent")); } @Test // Unfortunate - this should work as well public void multicastGenericEventWildcardSubType() { - multicastEvent(false, StringEventListener.class, createGenericEvent("test"), + multicastEvent(false, StringEventListener.class, createGenericTestEvent("test"), getGenericApplicationEventType("wildcardEvent")); } 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 new file mode 100644 index 0000000000..0978494041 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java @@ -0,0 +1,345 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.PayloadApplicationEvent; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.Order; +import org.springframework.util.ReflectionUtils; + +import static org.hamcrest.Matchers.*; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * @author Stephane Nicoll + */ +public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEventListenerTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final SampleEvents sampleEvents = spy(new SampleEvents()); + + private final ApplicationContext context = mock(ApplicationContext.class); + + @Test + public void rawListener() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleRaw", ApplicationEvent.class); + supportsEventType(true, method, getGenericApplicationEventType("applicationEvent")); + } + + @Test + public void rawListenerWithGenericEvent() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleRaw", ApplicationEvent.class); + supportsEventType(true, method, getGenericApplicationEventType("stringEvent")); + } + + @Test + public void genericListener() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleGenericString", GenericTestEvent.class); + supportsEventType(true, method, getGenericApplicationEventType("stringEvent")); + } + + @Test + public void genericListenerWrongParameterizedType() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleGenericString", GenericTestEvent.class); + supportsEventType(false, method, getGenericApplicationEventType("longEvent")); + } + + @Test + public void listenerWithPayloadAndGenericInformation() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleString", String.class); + supportsEventType(true, method, createGenericEventType(String.class)); + } + + @Test + public void listenerWithInvalidPayloadAndGenericInformation() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleString", String.class); + supportsEventType(false, method, createGenericEventType(Integer.class)); + } + + @Test + public void listenerWithPayloadTypeErasure() { // Always accept such event when the type is unknown + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleString", String.class); + supportsEventType(true, method, ResolvableType.forClass(PayloadApplicationEvent.class)); + } + + @Test + public void listenerWithSubTypeSeveralGenerics() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleString", String.class); + supportsEventType(true, method, ResolvableType.forClass(PayloadTestEvent.class)); + } + + @Test + public void listenerWithSubTypeSeveralGenericsResolved() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleString", String.class); + supportsEventType(true, method, ResolvableType.forClass(PayloadStringTestEvent.class)); + } + + @Test + public void listenerWithTooManyParameters() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "tooManyParameters", String.class, String.class); + + thrown.expect(IllegalStateException.class); + createTestInstance(method); + } + + @Test + public void listenerWithNoParameter() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "noParameter"); + + thrown.expect(IllegalStateException.class); + createTestInstance(method); + } + + @Test + public void defaultOrder() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleGenericString", GenericTestEvent.class); + ApplicationListenerMethodAdapter adapter = createTestInstance(method); + assertEquals(0, adapter.getOrder()); + } + + @Test + public void specifiedOrder() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleRaw", ApplicationEvent.class); + ApplicationListenerMethodAdapter adapter = createTestInstance(method); + assertEquals(42, adapter.getOrder()); + } + + @Test + public void invokeListener() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleGenericString", GenericTestEvent.class); + GenericTestEvent event = createGenericTestEvent("test"); + invokeListener(method, event); + verify(this.sampleEvents, times(1)).handleGenericString(event); + } + + @Test + public void invokeListenerRuntimeException() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "generateRuntimeException", GenericTestEvent.class); + GenericTestEvent event = createGenericTestEvent("fail"); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Test exception"); + thrown.expectCause(is(isNull(Throwable.class))); + invokeListener(method, event); + } + + @Test + public void invokeListenerCheckedException() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "generateCheckedException", GenericTestEvent.class); + GenericTestEvent event = createGenericTestEvent("fail"); + + thrown.expect(UndeclaredThrowableException.class); + thrown.expectCause(is(instanceOf(IOException.class))); + invokeListener(method, event); + } + + @Test + public void invokeListenerInvalidProxy() { + Object target = new InvalidProxyTestBean(); + ProxyFactory proxyFactory = new ProxyFactory(); + proxyFactory.setTarget(target); + proxyFactory.addInterface(SimpleService.class); + Object bean = proxyFactory.getProxy(getClass().getClassLoader()); + + Method method = ReflectionUtils.findMethod(InvalidProxyTestBean.class, "handleIt2", ApplicationEvent.class); + StaticApplicationListenerMethodAdapter listener = + new StaticApplicationListenerMethodAdapter(method, bean); + thrown.expect(IllegalStateException.class); + thrown.expectMessage("handleIt2"); + listener.onApplicationEvent(createGenericTestEvent("test")); + } + + @Test + public void invokeListenerWithPayload() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleString", String.class); + PayloadApplicationEvent event = new PayloadApplicationEvent<>(this, "test"); + invokeListener(method, event); + verify(this.sampleEvents, times(1)).handleString("test"); + } + + @Test + public void invokeListenerWithPayloadWrongType() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleString", String.class); + PayloadApplicationEvent event = new PayloadApplicationEvent<>(this, 123L); + invokeListener(method, event); + verify(this.sampleEvents, never()).handleString(anyString()); + } + + @Test + public void beanInstanceRetrievedAtEveryInvocation() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleGenericString", GenericTestEvent.class); + when(this.context.getBean("testBean")).thenReturn(this.sampleEvents); + ApplicationListenerMethodAdapter listener = new ApplicationListenerMethodAdapter( + "testBean", GenericTestEvent.class, method); + listener.init(this.context, new EventExpressionEvaluator()); + GenericTestEvent event = createGenericTestEvent("test"); + + + listener.onApplicationEvent(event); + verify(this.sampleEvents, times(1)).handleGenericString(event); + verify(this.context, times(1)).getBean("testBean"); + + listener.onApplicationEvent(event); + verify(this.sampleEvents, times(2)).handleGenericString(event); + verify(this.context, times(2)).getBean("testBean"); + } + + 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 void invokeListener(Method method, ApplicationEvent event) { + ApplicationListenerMethodAdapter adapter = createTestInstance(method); + adapter.onApplicationEvent(event); + } + + private ApplicationListenerMethodAdapter createTestInstance(Method method) { + return new StaticApplicationListenerMethodAdapter(method, this.sampleEvents); + } + + private ResolvableType createGenericEventType(Class payloadType) { + return ResolvableType.forClassWithGenerics(PayloadApplicationEvent.class, payloadType); + } + + private static class StaticApplicationListenerMethodAdapter + extends ApplicationListenerMethodAdapter { + + private final Object targetBean; + + public StaticApplicationListenerMethodAdapter(Method method, Object targetBean) { + super("unused", targetBean.getClass(), method); + this.targetBean = targetBean; + } + + @Override + public Object getTargetBean() { + return targetBean; + } + } + + + private static class SampleEvents { + + + @EventListener + @Order(42) + public void handleRaw(ApplicationEvent event) { + } + + @EventListener + public void handleGenericString(GenericTestEvent event) { + } + + @EventListener + public void handleString(String payload) { + } + + @EventListener + public void tooManyParameters(String event, String whatIsThis) { + } + + @EventListener + public void noParameter() { + } + + @EventListener + public void generateRuntimeException(GenericTestEvent event) { + if ("fail".equals(event.getPayload())) { + throw new IllegalStateException("Test exception"); + } + } + + @EventListener + public void generateCheckedException(GenericTestEvent event) throws IOException { + if ("fail".equals(event.getPayload())) { + throw new IOException("Test exception"); + } + } + } + + interface SimpleService { + + void handleIt(ApplicationEvent event); + + } + + static class InvalidProxyTestBean implements SimpleService { + + @Override + public void handleIt(ApplicationEvent event) { + } + + @EventListener + public void handleIt2(ApplicationEvent event) { + } + } + + @SuppressWarnings({"unused", "serial"}) + static class PayloadTestEvent extends PayloadApplicationEvent { + + private final V something; + + public PayloadTestEvent(Object source, T payload, V something) { + super(source, payload); + this.something = something; + } + } + + @SuppressWarnings({"unused", "serial"}) + static class PayloadStringTestEvent extends PayloadTestEvent { + public PayloadStringTestEvent(Object source, String payload, Long something) { + super(source, payload, something); + } + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/EventPublicationInterceptorTests.java b/spring-context/src/test/java/org/springframework/context/event/EventPublicationInterceptorTests.java index 099dc28260..0cb677c3fc 100644 --- a/spring-context/src/test/java/org/springframework/context/event/EventPublicationInterceptorTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/EventPublicationInterceptorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.TestListener; +import org.springframework.context.event.test.TestEvent; import org.springframework.context.support.StaticApplicationContext; import org.springframework.tests.sample.beans.ITestBean; import org.springframework.tests.sample.beans.TestBean; @@ -116,15 +117,6 @@ public class EventPublicationInterceptorTests { } - @SuppressWarnings("serial") - public static class TestEvent extends ApplicationEvent { - - public TestEvent(Object source) { - super(source); - } - } - - @SuppressWarnings("serial") public static final class TestEventWithNoValidOneArgObjectCtor extends ApplicationEvent { diff --git a/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java index 744ecf49e7..20243c8eb8 100644 --- a/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java @@ -54,7 +54,7 @@ public class GenericApplicationListenerAdapterTests extends AbstractApplicationE @Test // Demonstrates we can't inject that event because the generic type is lost public void genericListenerStrictTypeTypeErasure() { - GenericApplicationEvent stringEvent = createGenericEvent("test"); + GenericTestEvent stringEvent = createGenericTestEvent("test"); ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); supportsEventType(false, StringEventListener.class, eventType); } @@ -62,7 +62,7 @@ public class GenericApplicationListenerAdapterTests extends AbstractApplicationE @Test // But it works if we specify the type properly public void genericListenerStrictTypeAndResolvableType() { ResolvableType eventType = ResolvableType - .forClassWithGenerics(GenericApplicationEvent.class, String.class); + .forClassWithGenerics(GenericTestEvent.class, String.class); supportsEventType(true, StringEventListener.class, eventType); } @@ -87,7 +87,7 @@ public class GenericApplicationListenerAdapterTests extends AbstractApplicationE @Test public void genericListenerStrictTypeNotMatchTypeErasure() { - GenericApplicationEvent longEvent = createGenericEvent(123L); + GenericTestEvent longEvent = createGenericTestEvent(123L); ResolvableType eventType = ResolvableType.forType(longEvent.getClass()); supportsEventType(false, StringEventListener.class, eventType); } @@ -118,7 +118,7 @@ public class GenericApplicationListenerAdapterTests extends AbstractApplicationE @Test // Demonstrates we cant inject that event because the listener has a wildcard public void genericListenerWildcardTypeTypeErasure() { - GenericApplicationEvent stringEvent = createGenericEvent("test"); + GenericTestEvent stringEvent = createGenericTestEvent("test"); ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); supportsEventType(true, GenericEventListener.class, eventType); } @@ -131,7 +131,7 @@ public class GenericApplicationListenerAdapterTests extends AbstractApplicationE @Test // Demonstrates we cant inject that event because the listener has a raw type public void genericListenerRawTypeTypeErasure() { - GenericApplicationEvent stringEvent = createGenericEvent("test"); + GenericTestEvent stringEvent = createGenericTestEvent("test"); ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); supportsEventType(true, RawApplicationListener.class, eventType); } diff --git a/spring-context/src/test/java/org/springframework/context/event/test/AbstractIdentifiable.java b/spring-context/src/test/java/org/springframework/context/event/test/AbstractIdentifiable.java new file mode 100644 index 0000000000..b960d8aa9b --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/AbstractIdentifiable.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event.test; + +import java.util.UUID; + +/** + * @author Stephane Nicoll + */ +public abstract class AbstractIdentifiable implements Identifiable { + + private final String id; + + public AbstractIdentifiable() { + this.id = UUID.randomUUID().toString(); + } + + @Override + public String getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AbstractIdentifiable that = (AbstractIdentifiable) o; + + return id.equals(that.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/AnotherTestEvent.java b/spring-context/src/test/java/org/springframework/context/event/test/AnotherTestEvent.java new file mode 100644 index 0000000000..59cae5a0f5 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/AnotherTestEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event.test; + +/** + * @author Stephane Nicoll + */ +@SuppressWarnings("serial") +public class AnotherTestEvent extends IdentifiableApplicationEvent { + + public final String msg; + + public AnotherTestEvent(Object source, String msg) { + super(source); + this.msg = msg; + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/EventCollector.java b/spring-context/src/test/java/org/springframework/context/event/test/EventCollector.java new file mode 100644 index 0000000000..78dddb96b0 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/EventCollector.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event.test; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.junit.Assert.*; + +/** + * Test utility to collect and assert events. + * + * @author Stephane Nicoll + */ +@Component +public class EventCollector { + + private final MultiValueMap content = new LinkedMultiValueMap<>(); + + + /** + * Register an event for the specified listener. + */ + public void addEvent(Identifiable listener, Object event) { + this.content.add(listener.getId(), event); + } + + /** + * Return the events that the specified listener has received. The list of events + * is ordered according to their reception order. + */ + public List getEvents(Identifiable listener) { + return this.content.get(listener.getId()); + } + + /** + * Assert that the listener identified by the specified id has not received any event. + */ + public void assertNoEventReceived(String listenerId) { + List events = content.getOrDefault(listenerId, Collections.emptyList()); + assertEquals("Expected no events but got " + events, 0, events.size()); + } + + /** + * Assert that the specified listener has not received any event. + */ + public void assertNoEventReceived(Identifiable listener) { + assertNoEventReceived(listener.getId()); + } + + /** + * Assert that the listener identified by the specified id has received the + * specified events, in that specific order. + */ + public void assertEvent(String listenerId, Object... events) { + List actual = content.getOrDefault(listenerId, Collections.emptyList()); + assertEquals("wrong number of events", events.length, actual.size()); + for (int i = 0; i < events.length; i++) { + assertEquals("Wrong event at index " + i, events[i], actual.get(i)); + } + } + + /** + * Assert that the specified listener has received the specified events, in + * that specific order. + */ + public void assertEvent(Identifiable listener, Object... events) { + assertEvent(listener.getId(), events); + } + + /** + * Assert the number of events received by this instance. Checks that + * unexpected events have not been received. If an event is handled by + * several listeners, each instance will be registered. + */ + public void assertTotalEventsCount(int number) { + int actual = 0; + for (Map.Entry> entry : this.content.entrySet()) { + actual += entry.getValue().size(); + } + assertEquals("Wrong number of total events (" + this.content.size() + ") " + + "registered listener(s)", number, actual); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/Identifiable.java b/spring-context/src/test/java/org/springframework/context/event/test/Identifiable.java new file mode 100644 index 0000000000..85983ed5de --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/Identifiable.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event.test; + +/** + * A simple marker interface used to identify an event or an event listener + * + * @author Stephane Nicoll + */ +public interface Identifiable { + + /** + * Return a unique global id used to identify this instance. + */ + String getId(); + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/IdentifiableApplicationEvent.java b/spring-context/src/test/java/org/springframework/context/event/test/IdentifiableApplicationEvent.java new file mode 100644 index 0000000000..8694c97774 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/IdentifiableApplicationEvent.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event.test; + +import java.util.UUID; + +import org.springframework.context.ApplicationEvent; + +/** + * A basic test event that can be uniquely identified easily. + * + * @author Stephane Nicoll + */ +@SuppressWarnings("serial") +public abstract class IdentifiableApplicationEvent extends ApplicationEvent implements Identifiable { + + private final String id; + + protected IdentifiableApplicationEvent(Object source, String id) { + super(source); + this.id = id; + } + + protected IdentifiableApplicationEvent(Object source) { + this(source, UUID.randomUUID().toString()); + } + + protected IdentifiableApplicationEvent() { + this(new Object()); + } + + @Override + public String getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + IdentifiableApplicationEvent that = (IdentifiableApplicationEvent) o; + + return id.equals(that.id); + + } + + @Override + public int hashCode() { + return id.hashCode(); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/test/TestEvent.java b/spring-context/src/test/java/org/springframework/context/event/test/TestEvent.java new file mode 100644 index 0000000000..825ea1606a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/test/TestEvent.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event.test; + +/** + * @author Stephane Nicoll + */ +@SuppressWarnings("serial") +public class TestEvent extends IdentifiableApplicationEvent { + + public final String msg; + + public TestEvent(Object source, String id, String msg) { + super(source, id); + this.msg = msg; + } + + public TestEvent(Object source, String msg) { + super(source); + this.msg = msg; + } + + public TestEvent(Object source) { + this(source, "test"); + } + + public TestEvent() { + this(new Object()); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/expression/MethodBasedEvaluationContextTest.java b/spring-context/src/test/java/org/springframework/context/expression/MethodBasedEvaluationContextTest.java new file mode 100644 index 0000000000..01f9da8c37 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/expression/MethodBasedEvaluationContextTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.expression; + +import java.lang.reflect.Method; + +import org.junit.Test; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.util.ReflectionUtils; + +import static org.junit.Assert.*; + +/** + * @author Stephane Nicoll + */ +public class MethodBasedEvaluationContextTest { + + private final ParameterNameDiscoverer paramDiscover = new DefaultParameterNameDiscoverer(); + + @Test + public void simpleArguments() { + Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", + String.class, Boolean.class); + MethodBasedEvaluationContext context = createEvaluationContext(method, new Object[] {"test", true}); + + assertEquals("test", context.lookupVariable("a0")); + assertEquals("test", context.lookupVariable("p0")); + assertEquals("test", context.lookupVariable("foo")); + + assertEquals(true, context.lookupVariable("a1")); + assertEquals(true, context.lookupVariable("p1")); + assertEquals(true, context.lookupVariable("flag")); + + assertNull(context.lookupVariable("a2")); + } + + @Test + public void nullArgument() { + Method method = ReflectionUtils.findMethod(SampleMethods.class, "hello", + String.class, Boolean.class); + MethodBasedEvaluationContext context = createEvaluationContext(method, new Object[] {null, null}); + + assertNull(context.lookupVariable("a0")); + assertNull(context.lookupVariable("p0")); + } + + private MethodBasedEvaluationContext createEvaluationContext(Method method, Object[] args) { + return new MethodBasedEvaluationContext(this, method, args, this.paramDiscover); + } + + + @SuppressWarnings("unused") + private static class SampleMethods { + + private void hello(String foo, Boolean flag) { + } + + } + +} \ No newline at end of file diff --git a/spring-context/src/test/resources/org/springframework/context/event/simple-event-configuration.xml b/spring-context/src/test/resources/org/springframework/context/event/simple-event-configuration.xml new file mode 100644 index 0000000000..74cdf0679a --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/event/simple-event-configuration.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/broker/BrokerMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/broker/BrokerMessageHandlerTests.java index aac981c542..fa655104e2 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/broker/BrokerMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/broker/BrokerMessageHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -146,6 +146,11 @@ public class BrokerMessageHandlerTests { @Override public void publishEvent(ApplicationEvent event) { + publishEvent((Object) event); + } + + @Override + public void publishEvent(Object event) { if (event instanceof BrokerAvailabilityEvent) { this.availabilityEvents.add(((BrokerAvailabilityEvent) event).isBrokerAvailable()); } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerIntegrationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerIntegrationTests.java index f7ec40d95a..c84dc71dab 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerIntegrationTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -252,6 +252,11 @@ public class StompBrokerRelayMessageHandlerIntegrationTests { @Override public void publishEvent(ApplicationEvent event) { + publishEvent((Object) event); + } + + @Override + public void publishEvent(Object event) { logger.debug("Processing ApplicationEvent " + event); if (event instanceof BrokerAvailabilityEvent) { this.eventQueue.add((BrokerAvailabilityEvent) event); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StubWebApplicationContext.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StubWebApplicationContext.java index ea73248eb7..c98e4752f1 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StubWebApplicationContext.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StubWebApplicationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -317,6 +317,10 @@ class StubWebApplicationContext implements WebApplicationContext { public void publishEvent(ApplicationEvent event) { } + @Override + public void publishEvent(Object event) { + } + @Override public Resource[] getResources(String locationPattern) throws IOException { return this.resourcePatternResolver.getResources(locationPattern); diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompSubProtocolHandlerTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompSubProtocolHandlerTests.java index 8030a3833d..c8ab1f0b3b 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompSubProtocolHandlerTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/messaging/StompSubProtocolHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,6 +44,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.PayloadApplicationEvent; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.simp.SimpAttributes; @@ -448,6 +449,11 @@ public class StompSubProtocolHandlerTests { public void publishEvent(ApplicationEvent event) { events.add(event); } + + @Override + public void publishEvent(Object event) { + publishEvent(new PayloadApplicationEvent(this, event)); + } } }