Annotation-based event listeners

Add support for annotation-based event listeners. Enabled automatically
when using Java configuration or can be enabled explicitly via the
regular <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-11622
This commit is contained in:
Stephane Nicoll
2015-01-23 11:57:58 +01:00
parent 6d6422acde
commit f0fca890bb
31 changed files with 2288 additions and 134 deletions

View File

@@ -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<AnnotatedElementKey, Method> methodCache;
class CacheEvaluationContext extends MethodBasedEvaluationContext {
private final List<String> 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<AnnotatedElementKey, Method> 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<String>();
}
@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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 <strong>matching</strong> listeners registered with this
* application of an event.
* <p>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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 "";
}

View File

@@ -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;
}
}

View File

@@ -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]);
}
}
}
}

View File

@@ -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<Object>(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);