Browse Source
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
pull/723/merge
13 changed files with 507 additions and 68 deletions
@ -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. |
||||||
|
* |
||||||
|
* <p>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<ApplicationEvent>, 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); |
||||||
|
|
||||||
|
} |
||||||
@ -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; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue