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)); + } } }