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<GenericEvent<String>> { ... }
This listener should only receive events that are matching the generic
signature, for instance:
public class StringEvent extends GenericEvent<String> { ... }
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<String>" 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
This commit is contained in:
@@ -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<T>
|
||||
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<String> {
|
||||
|
||||
public StringEvent(Object source, String payload) {
|
||||
super(source, payload);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class LongEvent extends GenericApplicationEvent<Long> {
|
||||
|
||||
public LongEvent(Object source, Long payload) {
|
||||
super(source, payload);
|
||||
}
|
||||
}
|
||||
|
||||
protected <T> GenericApplicationEvent<T> createGenericEvent(T payload) {
|
||||
return new GenericApplicationEvent<>(this, payload);
|
||||
}
|
||||
|
||||
|
||||
static class GenericEventListener implements ApplicationListener<GenericApplicationEvent<?>> {
|
||||
@Override
|
||||
public void onApplicationEvent(GenericApplicationEvent<?> event) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
static class ObjectEventListener implements ApplicationListener<GenericApplicationEvent<Object>> {
|
||||
@Override
|
||||
public void onApplicationEvent(GenericApplicationEvent<Object> event) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
static class UpperBoundEventListener
|
||||
implements ApplicationListener<GenericApplicationEvent<? extends RuntimeException>> {
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(GenericApplicationEvent<? extends RuntimeException> event) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
static class StringEventListener implements ApplicationListener<GenericApplicationEvent<String>> {
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(GenericApplicationEvent<String> event) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
static class RawApplicationListener implements ApplicationListener {
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationEvent event) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
static class GenericApplicationEvents {
|
||||
|
||||
public GenericApplicationEvent<?> wildcardEvent;
|
||||
|
||||
public GenericApplicationEvent<String> stringEvent;
|
||||
|
||||
public GenericApplicationEvent<Long> longEvent;
|
||||
|
||||
public GenericApplicationEvent<IllegalStateException> illegalStateExceptionEvent;
|
||||
|
||||
public GenericApplicationEvent<IOException> ioExceptionEvent;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ApplicationEvent> 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<ApplicationEvent> listener =
|
||||
(ApplicationListener<ApplicationEvent>) 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
|
||||
|
||||
@@ -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<String> 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<Long> 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<String> 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<String> stringEvent = createGenericEvent("test");
|
||||
ResolvableType eventType = ResolvableType.forType(stringEvent.getClass());
|
||||
supportsEventType(true, RawApplicationListener.class, eventType);
|
||||
}
|
||||
|
||||
private void supportsEventType(boolean match, Class<? extends ApplicationListener> 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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user