Browse Source
Add support for annotation-based event listeners. Enabled automatically when using Java configuration or can be enabled explicitly via the regular <context:annotation-driven/> XML element. Detect methods of managed beans annotated with @EventListener, either directly or through a meta-annotation. Annotated methods must define the event type they listen to as a single parameter argument. Events are automatically filtered out according to the method signature. When additional runtime filtering is required, one can specify the `condition` attribute of the annotation that defines a SpEL expression that should match to actually invoke the method for a particular event. The root context exposes the actual `event` (`#root.event`) and method arguments (`#root.args`). Individual method arguments are also exposed via either the `a` or `p` alias (`#a0` refers to the first method argument). Finally, methods arguments are exposed via their names if that information can be discovered. Events can be either an ApplicationEvent or any arbitrary payload. Such payload is wrapped automatically in a PayloadApplicationEvent and managed explicitly internally. As a result, users can now publish and listen for arbitrary objects. If an annotated method has a return value, an non null result is actually published as a new event, something like: @EventListener public FooEvent handle(BarEvent event) { ... } Events can be handled in an aynchronous manner by adding `@Async` to the event method declaration and enabling such infrastructure. Events can also be ordered by adding an `@Order` annotation to the event method. Issue: SPR-11622pull/723/merge
31 changed files with 2288 additions and 134 deletions
@ -0,0 +1,50 @@
@@ -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. |
||||
* <p> |
||||
* Mainly intended for internal use within the framework. |
||||
* |
||||
* @param <T> the payload type of the event |
||||
* @author Stephane Nicoll |
||||
* @since 4.2 |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
public class PayloadApplicationEvent<T> |
||||
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; |
||||
} |
||||
|
||||
} |
||||
|
@ -0,0 +1,268 @@
@@ -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. |
||||
* |
||||
* <p>Unwrap the content of a {@link PayloadApplicationEvent} if necessary |
||||
* to allow method declaration to define any arbitrary event type. |
||||
* |
||||
* <p>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}. |
||||
* <p>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(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,85 @@
@@ -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<ExpressionKey, Expression> conditionCache = new ConcurrentHashMap<ExpressionKey, Expression>(64); |
||||
|
||||
private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<AnnotatedElementKey, Method>(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; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,46 @@
@@ -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; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,63 @@
@@ -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. |
||||
* |
||||
* <p>Processing of {@code @EventListener} annotations is performed via |
||||
* {@link EventListenerMethodProcessor} that is registered automatically |
||||
* when using Java config or via the {@code <context:annotation-driven/>} |
||||
* XML element. |
||||
* |
||||
* <p>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. |
||||
* |
||||
* <p>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. |
||||
* <p>Default is "", meaning the event is always handled. |
||||
*/ |
||||
String condition() default ""; |
||||
|
||||
} |
@ -0,0 +1,149 @@
@@ -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<Class<?>> nonAnnotatedClasses = |
||||
Collections.newSetFromMap(new ConcurrentHashMap<Class<?>, 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<Method> annotatedMethods = new LinkedHashSet<Method>(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; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,96 @@
@@ -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. |
||||
* <p> |
||||
* Expose the actual method arguments using the following aliases: |
||||
* <ol> |
||||
* <li>pX where X is the index of the argument (p0 for the first argument)</li> |
||||
* <li>aX where X is the index of the argument (a1 for the second argument)</li> |
||||
* <li>the name of the parameter as discovered by a configurable {@link ParameterNameDiscoverer}</li> |
||||
* </ol> |
||||
* |
||||
* @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]); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,619 @@
@@ -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<Object> 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<Object> 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<Class<?>> 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<Class<?>> 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<String> 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<String> 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"); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,345 @@
@@ -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<String> 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<String> 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<String> 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<String> 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<Long> 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<String> 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<String> event) { |
||||
} |
||||
|
||||
@EventListener |
||||
public void handleString(String payload) { |
||||
} |
||||
|
||||
@EventListener |
||||
public void tooManyParameters(String event, String whatIsThis) { |
||||
} |
||||
|
||||
@EventListener |
||||
public void noParameter() { |
||||
} |
||||
|
||||
@EventListener |
||||
public void generateRuntimeException(GenericTestEvent<String> event) { |
||||
if ("fail".equals(event.getPayload())) { |
||||
throw new IllegalStateException("Test exception"); |
||||
} |
||||
} |
||||
|
||||
@EventListener |
||||
public void generateCheckedException(GenericTestEvent<String> 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<V, T> extends PayloadApplicationEvent<T> { |
||||
|
||||
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<Long, String> { |
||||
public PayloadStringTestEvent(Object source, String payload, Long something) { |
||||
super(source, payload, something); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,52 @@
@@ -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(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,32 @@
@@ -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; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,104 @@
@@ -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<String, Object> 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<Object> 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<Object> 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<Object> 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<String, List<Object>> entry : this.content.entrySet()) { |
||||
actual += entry.getValue().size(); |
||||
} |
||||
assertEquals("Wrong number of total events (" + this.content.size() + ") " + |
||||
"registered listener(s)", number, actual); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,31 @@
@@ -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(); |
||||
|
||||
} |
@ -0,0 +1,67 @@
@@ -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(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,45 @@
@@ -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()); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,76 @@
@@ -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) { |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<beans xmlns="http://www.springframework.org/schema/beans" |
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xmlns:context="http://www.springframework.org/schema/context" |
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans |
||||
http://www.springframework.org/schema/beans/spring-beans.xsd |
||||
http://www.springframework.org/schema/context |
||||
http://www.springframework.org/schema/context/spring-context.xsd"> |
||||
|
||||
<context:annotation-config/> |
||||
|
||||
<bean id="eventCollector" class="org.springframework.context.event.test.EventCollector"/> |
||||
|
||||
<bean id="testEventListener" |
||||
class="org.springframework.context.event.AnnotationDrivenEventListenerTests$TestEventListener"/> |
||||
|
||||
</beans> |
Loading…
Reference in new issue