From 6d6422acdef00a647af0c38d7d8309beb4187755 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Thu, 22 Jan 2015 14:47:30 +0100 Subject: [PATCH] Support for generics-based events Update the event publishing infrastructure to support generics-based events, that is support ApplicationListener implementations that define a generic event, something like: public class MyListener implements ApplicationListener> { ... } This listener should only receive events that are matching the generic signature, for instance: public class StringEvent extends GenericEvent { ... } Note that because of type erasure, publishing an event that defines the generic type at the instance level will not work. In other words, publishing "new GenericEvent" will not work as expected as type erasure will define it as GenericEvent. To support this feature, use the new GenericApplicationListener that supersedes SmartApplicationListener to handle generics-based even types via `supportsEventType` that takes a ResolvableType instance instead of the simple Class of the event. ApplicationEventMulticaster has an additional method to multicast an event based on the event and its ResolvableType. Issue: SPR-8201 --- .../context/ApplicationEventPublisher.java | 8 +- .../AbstractApplicationEventMulticaster.java | 56 ++++--- .../event/ApplicationEventMulticaster.java | 15 +- .../event/GenericApplicationListener.java | 46 ++++++ .../GenericApplicationListenerAdapter.java | 54 +++++-- .../SimpleApplicationEventMulticaster.java | 18 ++- .../event/SmartApplicationListener.java | 7 +- .../event/SourceFilteringListener.java | 14 +- .../support/AbstractApplicationContext.java | 17 +- ...AbstractApplicationEventListenerTests.java | 129 +++++++++++++++ .../event/ApplicationContextEventTests.java | 56 ++++++- ...enericApplicationListenerAdapterTests.java | 148 ++++++++++++++++++ ...ticApplicationContextMulticasterTests.java | 7 +- 13 files changed, 507 insertions(+), 68 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java create mode 100644 spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java create mode 100644 spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java index 14dd368759..b314cf5aa2 100644 --- a/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java +++ b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2005 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,9 +30,9 @@ package org.springframework.context; public interface ApplicationEventPublisher { /** - * Notify all listeners registered with this application of an application - * event. Events may be framework events (such as RequestHandledEvent) - * or application-specific events. + * Notify all matching listeners registered with this + * application of an application event. Events may be framework events + * (such as RequestHandledEvent) or application-specific events. * @param event the event to publish * @see org.springframework.web.context.support.RequestHandledEvent */ diff --git a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java index 850e4df4cf..7fc13089f6 100644 --- a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.OrderComparator; +import org.springframework.core.ResolvableType; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -49,8 +50,9 @@ import org.springframework.util.ObjectUtils; * Alternative implementations could be more sophisticated in those respects. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 1.2.3 - * @see #getApplicationListeners(ApplicationEvent) + * @see #getApplicationListeners(ApplicationEvent, ResolvableType) * @see SimpleApplicationEventMulticaster */ public abstract class AbstractApplicationEventMulticaster @@ -145,13 +147,16 @@ public abstract class AbstractApplicationEventMulticaster * event type. Non-matching listeners get excluded early. * @param event the event to be propagated. Allows for excluding * non-matching listeners early, based on cached matching information. + * @param eventType the event type * @return a Collection of ApplicationListeners * @see org.springframework.context.ApplicationListener */ - protected Collection> getApplicationListeners(ApplicationEvent event) { + protected Collection> getApplicationListeners( + ApplicationEvent event, ResolvableType eventType) { + Object source = event.getSource(); Class sourceType = (source != null ? source.getClass() : null); - ListenerCacheKey cacheKey = new ListenerCacheKey(event.getClass(), sourceType); + ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType); // Quick check for existing entry on ConcurrentHashMap... ListenerRetriever retriever = this.retrieverCache.get(cacheKey); @@ -169,26 +174,28 @@ public abstract class AbstractApplicationEventMulticaster return retriever.getApplicationListeners(); } retriever = new ListenerRetriever(true); - Collection> listeners = retrieveApplicationListeners(event, sourceType, retriever); + Collection> listeners = + retrieveApplicationListeners(event, eventType, sourceType, retriever); this.retrieverCache.put(cacheKey, retriever); return listeners; } } else { // No ListenerRetriever caching -> no synchronization necessary - return retrieveApplicationListeners(event, sourceType, null); + return retrieveApplicationListeners(event, eventType, sourceType, null); } } /** * Actually retrieve the application listeners for the given event and source type. * @param event the application event + * @param eventType the event type * @param sourceType the event source type * @param retriever the ListenerRetriever, if supposed to populate one (for caching purposes) * @return the pre-filtered list of application listeners for the given event and source type */ private Collection> retrieveApplicationListeners( - ApplicationEvent event, Class sourceType, ListenerRetriever retriever) { + ApplicationEvent event, ResolvableType eventType, Class sourceType, ListenerRetriever retriever) { LinkedList> allListeners = new LinkedList>(); Set> listeners; @@ -198,7 +205,7 @@ public abstract class AbstractApplicationEventMulticaster listenerBeans = new LinkedHashSet(this.defaultRetriever.applicationListenerBeans); } for (ApplicationListener listener : listeners) { - if (supportsEvent(listener, event.getClass(), sourceType)) { + if (supportsEvent(listener, eventType, sourceType)) { if (retriever != null) { retriever.applicationListeners.add(listener); } @@ -210,10 +217,10 @@ public abstract class AbstractApplicationEventMulticaster for (String listenerBeanName : listenerBeans) { try { Class listenerType = beanFactory.getType(listenerBeanName); - if (listenerType == null || supportsEvent(listenerType, event)) { + if (listenerType == null || supportsEvent(listenerType, eventType)) { ApplicationListener listener = beanFactory.getBean(listenerBeanName, ApplicationListener.class); - if (!allListeners.contains(listener) && supportsEvent(listener, event.getClass(), sourceType)) { + if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType)) { if (retriever != null) { retriever.applicationListenerBeans.add(listenerBeanName); } @@ -236,26 +243,27 @@ public abstract class AbstractApplicationEventMulticaster * type before trying to instantiate it. *

If this method returns {@code true} for a given listener as a first pass, * the listener instance will get retrieved and fully evaluated through a - * {@link #supportsEvent(ApplicationListener, Class, Class)} call afterwards. + * {@link #supportsEvent(ApplicationListener,ResolvableType, Class)} call afterwards. * @param listenerType the listener's type as determined by the BeanFactory - * @param event the event to check + * @param eventType the event type to check * @return whether the given listener should be included in the candidates * for the given event type */ - protected boolean supportsEvent(Class listenerType, ApplicationEvent event) { - if (SmartApplicationListener.class.isAssignableFrom(listenerType)) { + protected boolean supportsEvent(Class listenerType, ResolvableType eventType) { + if (GenericApplicationListener.class.isAssignableFrom(listenerType) + || SmartApplicationListener.class.isAssignableFrom(listenerType)) { return true; } - Class declaredEventType = GenericApplicationListenerAdapter.resolveDeclaredEventType(listenerType); - return (declaredEventType == null || declaredEventType.isInstance(event)); + ResolvableType declaredEventType = GenericApplicationListenerAdapter.resolveDeclaredEventType(listenerType); + return (declaredEventType == null || declaredEventType.isAssignableFrom(eventType)); } /** * Determine whether the given listener supports the given event. *

The default implementation detects the {@link SmartApplicationListener} - * interface. In case of a standard {@link ApplicationListener}, a - * {@link GenericApplicationListenerAdapter} will be used to introspect - * the generically declared type of the target listener. + * and {@link GenericApplicationListener} interfaces. In case of a standard + * {@link ApplicationListener}, a {@link GenericApplicationListenerAdapter} + * will be used to introspect the generically declared type of the target listener. * @param listener the target listener to check * @param eventType the event type to check against * @param sourceType the source type to check against @@ -263,10 +271,10 @@ public abstract class AbstractApplicationEventMulticaster * for the given event type */ protected boolean supportsEvent(ApplicationListener listener, - Class eventType, Class sourceType) { + ResolvableType eventType, Class sourceType) { - SmartApplicationListener smartListener = (listener instanceof SmartApplicationListener ? - (SmartApplicationListener) listener : new GenericApplicationListenerAdapter(listener)); + GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ? + (GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener)); return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType)); } @@ -276,11 +284,11 @@ public abstract class AbstractApplicationEventMulticaster */ private static class ListenerCacheKey { - private final Class eventType; + private final ResolvableType eventType; private final Class sourceType; - public ListenerCacheKey(Class eventType, Class sourceType) { + public ListenerCacheKey(ResolvableType eventType, Class sourceType) { this.eventType = eventType; this.sourceType = sourceType; } diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java index 03bd1ceefe..9805323040 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 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. @@ -18,6 +18,7 @@ package org.springframework.context.event; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; +import org.springframework.core.ResolvableType; /** * Interface to be implemented by objects that can manage a number of @@ -29,6 +30,7 @@ import org.springframework.context.ApplicationListener; * * @author Rod Johnson * @author Juergen Hoeller + * @author Stephane Nicoll */ public interface ApplicationEventMulticaster { @@ -65,8 +67,19 @@ public interface ApplicationEventMulticaster { /** * Multicast the given application event to appropriate listeners. + *

Consider using {@link #multicastEvent(ApplicationEvent, ResolvableType)} + * if possible as it provides a better support for generics-based events. * @param event the event to multicast */ void multicastEvent(ApplicationEvent event); + /** + * Multicast the given application event to appropriate listeners. + *

If the {@code eventType} is {@code null}, a default type is built + * based on the {@code event} instance. + * @param event the event to multicast + * @param eventType the type of event (can be null) + */ + void multicastEvent(ApplicationEvent event, ResolvableType eventType); + } diff --git a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java new file mode 100644 index 0000000000..cfe7f457a9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListener.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.event; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; + +/** + * Extended variant of the standard {@link ApplicationListener} interface, + * exposing further metadata such as the supported event type. + * + *

As of Spring Framework 4.2, supersedes {@link SmartApplicationListener} with + * proper handling of generics-based event. + * + * @author Stephane Nicoll + * @since 4.2 + */ +public interface GenericApplicationListener extends ApplicationListener, Ordered { + + /** + * Determine whether this listener actually supports the given event type. + */ + boolean supportsEventType(ResolvableType eventType); + + /** + * Determine whether this listener actually supports the given source type. + */ + boolean supportsSourceType(Class sourceType); + +} diff --git a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerAdapter.java b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerAdapter.java index 0d46b5980a..9362ac7875 100644 --- a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,22 +19,24 @@ package org.springframework.context.event; import org.springframework.aop.support.AopUtils; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; -import org.springframework.core.GenericTypeResolver; import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; import org.springframework.util.Assert; /** - * {@link SmartApplicationListener} adapter that determines supported event types + * {@link GenericApplicationListener} adapter that determines supported event types * through introspecting the generically declared type of the target listener. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 3.0 * @see org.springframework.context.ApplicationListener#onApplicationEvent */ -public class GenericApplicationListenerAdapter implements SmartApplicationListener { +public class GenericApplicationListenerAdapter implements GenericApplicationListener { private final ApplicationListener delegate; + private final ResolvableType declaredEventType; /** * Create a new GenericApplicationListener for the given delegate. @@ -44,6 +46,7 @@ public class GenericApplicationListenerAdapter implements SmartApplicationListen public GenericApplicationListenerAdapter(ApplicationListener delegate) { Assert.notNull(delegate, "Delegate listener must not be null"); this.delegate = (ApplicationListener) delegate; + this.declaredEventType = resolveDeclaredEventType(this.delegate); } @@ -53,20 +56,25 @@ public class GenericApplicationListenerAdapter implements SmartApplicationListen } @Override - public boolean supportsEventType(Class eventType) { - Class declaredEventType = resolveDeclaredEventType(this.delegate.getClass()); - if (declaredEventType == null || declaredEventType.equals(ApplicationEvent.class)) { - Class targetClass = AopUtils.getTargetClass(this.delegate); - if (targetClass != this.delegate.getClass()) { - declaredEventType = resolveDeclaredEventType(targetClass); - } + @SuppressWarnings("unchecked") + public boolean supportsEventType(ResolvableType eventType) { + if (this.delegate instanceof SmartApplicationListener) { + Class eventClass = (Class) eventType.getRawClass(); + return ((SmartApplicationListener) this.delegate).supportsEventType(eventClass); + } + else { + return (this.declaredEventType == null || this.declaredEventType.isAssignableFrom(eventType)); } - return (declaredEventType == null || declaredEventType.isAssignableFrom(eventType)); } @Override public boolean supportsSourceType(Class sourceType) { - return true; + if (this.delegate instanceof SmartApplicationListener) { + return ((SmartApplicationListener) this.delegate).supportsSourceType(sourceType); + } + else { + return true; + } } @Override @@ -74,9 +82,25 @@ public class GenericApplicationListenerAdapter implements SmartApplicationListen return (this.delegate instanceof Ordered ? ((Ordered) this.delegate).getOrder() : Ordered.LOWEST_PRECEDENCE); } + static ResolvableType resolveDeclaredEventType(Class listenerType) { + ResolvableType resolvableType = ResolvableType.forClass(listenerType).as(ApplicationListener.class); + if (resolvableType == null || !resolvableType.hasGenerics()) { + return null; + } + return resolvableType.getGeneric(); + } + + private static ResolvableType resolveDeclaredEventType(ApplicationListener listener) { + ResolvableType declaredEventType = resolveDeclaredEventType(listener.getClass()); + if (declaredEventType == null || declaredEventType.isAssignableFrom( + ResolvableType.forClass(ApplicationEvent.class))) { - static Class resolveDeclaredEventType(Class listenerType) { - return GenericTypeResolver.resolveTypeArgument(listenerType, ApplicationListener.class); + Class targetClass = AopUtils.getTargetClass(listener); + if (targetClass != listener.getClass()) { + declaredEventType = resolveDeclaredEventType(targetClass); + } + } + return declaredEventType; } } diff --git a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java index d044aaf946..28587f21f4 100644 --- a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.util.concurrent.Executor; import org.springframework.beans.factory.BeanFactory; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; +import org.springframework.core.ResolvableType; import org.springframework.util.ErrorHandler; /** @@ -38,6 +39,7 @@ import org.springframework.util.ErrorHandler; * * @author Rod Johnson * @author Juergen Hoeller + * @author Stephane Nicoll * @see #setTaskExecutor */ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster { @@ -113,8 +115,14 @@ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventM @Override - public void multicastEvent(final ApplicationEvent event) { - for (final ApplicationListener listener : getApplicationListeners(event)) { + public void multicastEvent(ApplicationEvent event) { + multicastEvent(event, resolveDefaultEventType(event)); + } + + @Override + public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) { + ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event)); + for (final ApplicationListener listener : getApplicationListeners(event, type)) { Executor executor = getTaskExecutor(); if (executor != null) { executor.execute(new Runnable() { @@ -130,6 +138,10 @@ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventM } } + private ResolvableType resolveDefaultEventType(ApplicationEvent event) { + return ResolvableType.forType(event.getClass()); + } + /** * Invoke the given listener with the given event. * @param listener the ApplicationListener to invoke diff --git a/spring-context/src/main/java/org/springframework/context/event/SmartApplicationListener.java b/spring-context/src/main/java/org/springframework/context/event/SmartApplicationListener.java index 9fe22adaf1..5d50920754 100644 --- a/spring-context/src/main/java/org/springframework/context/event/SmartApplicationListener.java +++ b/spring-context/src/main/java/org/springframework/context/event/SmartApplicationListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 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. @@ -24,8 +24,13 @@ import org.springframework.core.Ordered; * Extended variant of the standard {@link ApplicationListener} interface, * exposing further metadata such as the supported event type. * + *

Users are strongly advised to use the {@link GenericApplicationListener} + * interface instead as it provides an improved detection of generics-based + * event types. + * * @author Juergen Hoeller * @since 3.0 + * @see GenericApplicationListener */ public interface SmartApplicationListener extends ApplicationListener, Ordered { diff --git a/spring-context/src/main/java/org/springframework/context/event/SourceFilteringListener.java b/spring-context/src/main/java/org/springframework/context/event/SourceFilteringListener.java index 93bf92f756..6143b48483 100644 --- a/spring-context/src/main/java/org/springframework/context/event/SourceFilteringListener.java +++ b/spring-context/src/main/java/org/springframework/context/event/SourceFilteringListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.context.event; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; /** * {@link org.springframework.context.ApplicationListener} decorator that filters @@ -29,13 +30,14 @@ import org.springframework.core.Ordered; * method instead of specifying a delegate listener. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.0.5 */ -public class SourceFilteringListener implements SmartApplicationListener { +public class SourceFilteringListener implements GenericApplicationListener { private final Object source; - private SmartApplicationListener delegate; + private GenericApplicationListener delegate; /** @@ -47,8 +49,8 @@ public class SourceFilteringListener implements SmartApplicationListener { */ public SourceFilteringListener(Object source, ApplicationListener delegate) { this.source = source; - this.delegate = (delegate instanceof SmartApplicationListener ? - (SmartApplicationListener) delegate : new GenericApplicationListenerAdapter(delegate)); + this.delegate = (delegate instanceof GenericApplicationListener ? + (GenericApplicationListener) delegate : new GenericApplicationListenerAdapter(delegate)); } /** @@ -71,7 +73,7 @@ public class SourceFilteringListener implements SmartApplicationListener { } @Override - public boolean supportsEventType(Class eventType) { + public boolean supportsEventType(ResolvableType eventType) { return (this.delegate == null || this.delegate.supportsEventType(eventType)); } diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index 75e5e3d873..aac94c976d 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -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. @@ -63,6 +63,7 @@ import org.springframework.context.event.SimpleApplicationEventMulticaster; import org.springframework.context.expression.StandardBeanExpressionResolver; import org.springframework.context.weaving.LoadTimeWeaverAware; import org.springframework.context.weaving.LoadTimeWeaverAwareProcessor; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; @@ -107,6 +108,7 @@ import org.springframework.util.ObjectUtils; * @author Rod Johnson * @author Juergen Hoeller * @author Mark Fisher + * @author Stephane Nicoll * @since January 21, 2001 * @see #refreshBeanFactory * @see #getBeanFactory @@ -324,13 +326,22 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader */ @Override public void publishEvent(ApplicationEvent event) { + publishEvent(event, null); + } + + protected void publishEvent(ApplicationEvent event, ResolvableType eventType) { Assert.notNull(event, "Event must not be null"); if (logger.isTraceEnabled()) { logger.trace("Publishing event in " + getDisplayName() + ": " + event); } - getApplicationEventMulticaster().multicastEvent(event); + getApplicationEventMulticaster().multicastEvent(event, eventType); if (this.parent != null) { - this.parent.publishEvent(event); + if (this.parent instanceof AbstractApplicationContext) { + ((AbstractApplicationContext) this.parent).publishEvent(event, eventType); + } + else { + this.parent.publishEvent(event); + } } } diff --git a/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java new file mode 100644 index 0000000000..98dd3ea1ca --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java @@ -0,0 +1,129 @@ +/* + * 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 org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.ResolvableType; + +/** + * @author Stephane Nicoll + */ +@SuppressWarnings("serial") +public abstract class AbstractApplicationEventListenerTests { + + protected ResolvableType getGenericApplicationEventType(String fieldName) { + try { + return ResolvableType.forField(GenericApplicationEvents.class.getField(fieldName)); + } + catch (NoSuchFieldException e) { + throw new IllegalStateException("No such field on Events '" + fieldName + "'"); + } + } + + protected static class GenericApplicationEvent + extends ApplicationEvent { + + private final T payload; + + public GenericApplicationEvent(Object source, T payload) { + super(source); + this.payload = payload; + } + + public T getPayload() { + return payload; + } + + } + + protected static class StringEvent extends GenericApplicationEvent { + + public StringEvent(Object source, String payload) { + super(source, payload); + } + } + + protected static class LongEvent extends GenericApplicationEvent { + + public LongEvent(Object source, Long payload) { + super(source, payload); + } + } + + protected GenericApplicationEvent createGenericEvent(T payload) { + return new GenericApplicationEvent<>(this, payload); + } + + + static class GenericEventListener implements ApplicationListener> { + @Override + public void onApplicationEvent(GenericApplicationEvent event) { + + } + } + + static class ObjectEventListener implements ApplicationListener> { + @Override + public void onApplicationEvent(GenericApplicationEvent event) { + + } + } + + static class UpperBoundEventListener + implements ApplicationListener> { + + @Override + public void onApplicationEvent(GenericApplicationEvent event) { + + } + } + + static class StringEventListener implements ApplicationListener> { + + @Override + public void onApplicationEvent(GenericApplicationEvent event) { + + } + } + + static class RawApplicationListener implements ApplicationListener { + @Override + public void onApplicationEvent(ApplicationEvent event) { + + } + } + + + @SuppressWarnings("unused") + static class GenericApplicationEvents { + + public GenericApplicationEvent wildcardEvent; + + public GenericApplicationEvent stringEvent; + + public GenericApplicationEvent longEvent; + + public GenericApplicationEvent illegalStateExceptionEvent; + + public GenericApplicationEvent ioExceptionEvent; + + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java index 16e635a60c..8c214bb7ca 100644 --- a/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java @@ -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. @@ -34,6 +34,7 @@ import org.springframework.context.BeanThatListens; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.Ordered; +import org.springframework.core.ResolvableType; import org.springframework.scheduling.support.TaskUtils; import org.springframework.tests.sample.beans.TestBean; @@ -45,20 +46,59 @@ import static org.mockito.BDDMockito.*; * * @author Alef Arendsen * @author Rick Evans + * @author Stephane Nicoll */ -public class ApplicationContextEventTests { +public class ApplicationContextEventTests extends AbstractApplicationEventListenerTests { @Test - public void simpleApplicationEventMulticaster() { - @SuppressWarnings("unchecked") - ApplicationListener listener = mock(ApplicationListener.class); - ApplicationEvent evt = new ContextClosedEvent(new StaticApplicationContext()); + public void multicastSimpleEvent() { + multicastEvent(true, ApplicationListener.class, + new ContextClosedEvent(new StaticApplicationContext()), null); + } + @Test + public void multicastGenericEvent() { + multicastEvent(true, StringEventListener.class, createGenericEvent("test"), + getGenericApplicationEventType("stringEvent")); + } + + @Test + public void multicastGenericEventWrongType() { + multicastEvent(false, StringEventListener.class, createGenericEvent(123L), + getGenericApplicationEventType("longEvent")); + } + + @Test // Unfortunate - this should work as well + public void multicastGenericEventWildcardSubType() { + multicastEvent(false, StringEventListener.class, createGenericEvent("test"), + getGenericApplicationEventType("wildcardEvent")); + } + + @Test + public void multicastConcreteTypeGenericListener() { + multicastEvent(true, StringEventListener.class, new StringEvent(this, "test"), null); + } + + @Test + public void multicastConcreteWrongTypeGenericListener() { + multicastEvent(false, StringEventListener.class, new LongEvent(this, 123L), null); + } + + private void multicastEvent(boolean match, Class listenerType, + ApplicationEvent event, ResolvableType eventType) { + @SuppressWarnings("unchecked") + ApplicationListener listener = + (ApplicationListener) mock(listenerType); SimpleApplicationEventMulticaster smc = new SimpleApplicationEventMulticaster(); smc.addApplicationListener(listener); - smc.multicastEvent(evt); - verify(listener).onApplicationEvent(evt); + if (eventType != null) { + smc.multicastEvent(event, eventType); + } else { + smc.multicastEvent(event); + } + int invocation = match ? 1 : 0; + verify(listener, times(invocation)).onApplicationEvent(event); } @Test diff --git a/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java new file mode 100644 index 0000000000..744ecf49e7 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java @@ -0,0 +1,148 @@ +/* + * 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.junit.Test; + +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.ResolvableType; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * @author Stephane Nicoll + */ +public class GenericApplicationListenerAdapterTests extends AbstractApplicationEventListenerTests { + + @Test + public void supportsEventTypeWithSmartApplicationListener() { + SmartApplicationListener smartListener = mock(SmartApplicationListener.class); + GenericApplicationListenerAdapter listener = new GenericApplicationListenerAdapter(smartListener); + ResolvableType type = ResolvableType.forClass(ApplicationEvent.class); + listener.supportsEventType(type); + verify(smartListener, times(1)).supportsEventType(ApplicationEvent.class); + } + + @Test + public void supportsSourceTypeWithSmartApplicationListener() { + SmartApplicationListener smartListener = mock(SmartApplicationListener.class); + GenericApplicationListenerAdapter listener = new GenericApplicationListenerAdapter(smartListener); + listener.supportsSourceType(Object.class); + verify(smartListener, times(1)).supportsSourceType(Object.class); + } + + @Test + public void genericListenerStrictType() { + supportsEventType(true, StringEventListener.class, getGenericApplicationEventType("stringEvent")); + } + + @Test // Demonstrates we can't inject that event because the generic type is lost + public void genericListenerStrictTypeTypeErasure() { + GenericApplicationEvent stringEvent = createGenericEvent("test"); + ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); + supportsEventType(false, StringEventListener.class, eventType); + } + + @Test // But it works if we specify the type properly + public void genericListenerStrictTypeAndResolvableType() { + ResolvableType eventType = ResolvableType + .forClassWithGenerics(GenericApplicationEvent.class, String.class); + supportsEventType(true, StringEventListener.class, eventType); + } + + @Test // Demonstrates it works if we actually use the subtype + public void genericListenerStrictTypeEventSubType() { + StringEvent stringEvent = new StringEvent(this, "test"); + ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); + supportsEventType(true, StringEventListener.class, eventType); + } + + @Test + public void genericListenerStrictTypeNotMatching() { + supportsEventType(false, StringEventListener.class, getGenericApplicationEventType("longEvent")); + } + + @Test + public void genericListenerStrictTypeEventSubTypeNotMatching() { + LongEvent stringEvent = new LongEvent(this, 123L); + ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); + supportsEventType(false, StringEventListener.class, eventType); + } + + @Test + public void genericListenerStrictTypeNotMatchTypeErasure() { + GenericApplicationEvent longEvent = createGenericEvent(123L); + ResolvableType eventType = ResolvableType.forType(longEvent.getClass()); + supportsEventType(false, StringEventListener.class, eventType); + } + + @Test + public void genericListenerStrictTypeSubClass() { + supportsEventType(false, ObjectEventListener.class, + getGenericApplicationEventType("longEvent")); + } + + @Test + public void genericListenerUpperBoundType() { + supportsEventType(true, UpperBoundEventListener.class, + getGenericApplicationEventType("illegalStateExceptionEvent")); + } + + @Test + public void genericListenerUpperBoundTypeNotMatching() throws NoSuchFieldException { + supportsEventType(false, UpperBoundEventListener.class, + getGenericApplicationEventType("ioExceptionEvent")); + } + + @Test + public void genericListenerWildcardType() { + supportsEventType(true, GenericEventListener.class, + getGenericApplicationEventType("stringEvent")); + } + + @Test // Demonstrates we cant inject that event because the listener has a wildcard + public void genericListenerWildcardTypeTypeErasure() { + GenericApplicationEvent stringEvent = createGenericEvent("test"); + ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); + supportsEventType(true, GenericEventListener.class, eventType); + } + + @Test + public void genericListenerRawType() { + supportsEventType(true, RawApplicationListener.class, + getGenericApplicationEventType("stringEvent")); + } + + @Test // Demonstrates we cant inject that event because the listener has a raw type + public void genericListenerRawTypeTypeErasure() { + GenericApplicationEvent stringEvent = createGenericEvent("test"); + ResolvableType eventType = ResolvableType.forType(stringEvent.getClass()); + supportsEventType(true, RawApplicationListener.class, eventType); + } + + private void supportsEventType(boolean match, Class listenerType, + ResolvableType eventType) { + + ApplicationListener listener = mock(listenerType); + GenericApplicationListenerAdapter adapter = new GenericApplicationListenerAdapter(listener); + assertEquals("Wrong match for event '" + eventType + "' on " + listenerType.getClass().getName(), + match, adapter.supportsEventType(eventType)); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java b/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java index f2f74bc35d..981b9ef883 100644 --- a/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import org.springframework.context.ApplicationEvent; import org.springframework.context.BeanThatListens; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.event.SimpleApplicationEventMulticaster; +import org.springframework.core.ResolvableType; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.EncodedResource; @@ -92,8 +93,8 @@ public class StaticApplicationContextMulticasterTests extends AbstractApplicatio private static int counter = 0; @Override - public void multicastEvent(ApplicationEvent event) { - super.multicastEvent(event); + public void multicastEvent(ApplicationEvent event, ResolvableType eventType) { + super.multicastEvent(event, eventType); counter++; } } -- GitLab