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:
Stephane Nicoll
2015-01-22 14:47:30 +01:00
parent f84c458aba
commit 6d6422acde
13 changed files with 507 additions and 68 deletions

View File

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

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.
@@ -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

View File

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

View File

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