diff --git a/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java b/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java index 29e044adff..4ecb26582f 100644 --- a/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java +++ b/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java @@ -16,6 +16,8 @@ package org.springframework.context; +import org.springframework.core.ResolvableType; +import org.springframework.core.ResolvableTypeProvider; import org.springframework.util.Assert; /** @@ -28,7 +30,7 @@ import org.springframework.util.Assert; * @param the payload type of the event */ @SuppressWarnings("serial") -public class PayloadApplicationEvent extends ApplicationEvent { +public class PayloadApplicationEvent extends ApplicationEvent implements ResolvableTypeProvider { private final T payload; @@ -44,6 +46,11 @@ public class PayloadApplicationEvent extends ApplicationEvent { this.payload = payload; } + @Override + public ResolvableType getResolvableType() { + return ResolvableType.forClassWithGenerics(getClass(), + ResolvableType.forInstance(getPayload())); + } /** * Return the payload of the event. diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index ef109691ae..c37e8eb831 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -124,10 +124,11 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe protected Object[] resolveArguments(ApplicationEvent event) { if (!ApplicationEvent.class.isAssignableFrom(this.declaredEventType.getRawClass()) && event instanceof PayloadApplicationEvent) { - @SuppressWarnings("rawtypes") - Object payload = ((PayloadApplicationEvent) event).getPayload(); - if (this.declaredEventType.isAssignableFrom(ResolvableType.forClass(payload.getClass()))) { - return new Object[] {payload}; + PayloadApplicationEvent payloadEvent = (PayloadApplicationEvent) event; + ResolvableType payloadType = payloadEvent.getResolvableType() + .as(PayloadApplicationEvent.class).getGeneric(0); + if (this.declaredEventType.isAssignableFrom(payloadType)) { + return new Object[] {payloadEvent.getPayload()}; } } else { 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 28587f21f4..aaba475567 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 @@ -139,7 +139,7 @@ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventM } private ResolvableType resolveDefaultEventType(ApplicationEvent event) { - return ResolvableType.forType(event.getClass()); + return ResolvableType.forInstance(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 index 7298428f07..72a2561b5c 100644 --- a/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java @@ -21,6 +21,7 @@ import java.io.IOException; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.ResolvableType; +import org.springframework.core.ResolvableTypeProvider; /** * @author Stephane Nicoll @@ -53,6 +54,23 @@ public abstract class AbstractApplicationEventListenerTests { } + protected static class SmartGenericTestEvent + extends GenericTestEvent implements ResolvableTypeProvider { + + private final ResolvableType resolvableType; + + public SmartGenericTestEvent(Object source, T payload) { + super(source, payload); + this.resolvableType = ResolvableType.forClassWithGenerics( + getClass(), payload.getClass()); + } + + @Override + public ResolvableType getResolvableType() { + return this.resolvableType; + } + } + protected static class StringEvent extends GenericTestEvent { public StringEvent(Object source, String payload) { 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 e7651dc36f..d4f5445c2f 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 @@ -75,7 +75,7 @@ public class ApplicationContextEventTests extends AbstractApplicationEventListen getGenericApplicationEventType("longEvent")); } - @Test // Unfortunate - this should work as well + @Test public void multicastGenericEventWildcardSubType() { multicastEvent(false, StringEventListener.class, createGenericTestEvent("test"), getGenericApplicationEventType("wildcardEvent")); @@ -91,6 +91,16 @@ public class ApplicationContextEventTests extends AbstractApplicationEventListen multicastEvent(false, StringEventListener.class, new LongEvent(this, 123L), null); } + @Test + public void multicastSmartGenericTypeGenericListener() { + multicastEvent(true, StringEventListener.class, new SmartGenericTestEvent<>(this, "test"), null); + } + + @Test + public void multicastSmartGenericWrongTypeGenericListener() { + multicastEvent(false, StringEventListener.class, new SmartGenericTestEvent<>(this, 123L), null); + } + private void multicastEvent(boolean match, Class listenerType, ApplicationEvent event, ResolvableType eventType) { @SuppressWarnings("unchecked") diff --git a/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java b/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java index 95a2bea20a..b1643b12c8 100644 --- a/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java @@ -29,6 +29,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEvent; import org.springframework.context.PayloadApplicationEvent; import org.springframework.core.ResolvableType; +import org.springframework.core.ResolvableTypeProvider; import org.springframework.core.annotation.Order; import org.springframework.util.ReflectionUtils; @@ -155,6 +156,42 @@ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEv verify(this.sampleEvents, times(1)).handleGenericString(event); } + @Test + public void invokeListenerWithGenericEvent() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleGenericString", GenericTestEvent.class); + GenericTestEvent event = new SmartGenericTestEvent<>(this, "test"); + invokeListener(method, event); + verify(this.sampleEvents, times(1)).handleGenericString(event); + } + + @Test + public void invokeListenerWithGenericPayload() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleGenericStringPayload", EntityWrapper.class); + EntityWrapper payload = new EntityWrapper<>("test"); + invokeListener(method, new PayloadApplicationEvent<>(this, payload)); + verify(this.sampleEvents, times(1)).handleGenericStringPayload(payload); + } + + @Test + public void invokeListenerWithWrongGenericPayload() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleGenericStringPayload", EntityWrapper.class); + EntityWrapper payload = new EntityWrapper<>(123); + invokeListener(method, new PayloadApplicationEvent<>(this, payload)); + verify(this.sampleEvents, times(0)).handleGenericStringPayload(any()); + } + + @Test + public void invokeListenerWithAnyGenericPayload() { + Method method = ReflectionUtils.findMethod(SampleEvents.class, + "handleGenericAnyPayload", EntityWrapper.class); + EntityWrapper payload = new EntityWrapper<>("test"); + invokeListener(method, new PayloadApplicationEvent<>(this, payload)); + verify(this.sampleEvents, times(1)).handleGenericAnyPayload(payload); + } + @Test public void invokeListenerRuntimeException() { Method method = ReflectionUtils.findMethod(SampleEvents.class, @@ -284,6 +321,16 @@ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEv public void handleString(String payload) { } + @EventListener + public void handleGenericStringPayload(EntityWrapper event) { + + } + + @EventListener + public void handleGenericAnyPayload(EntityWrapper event) { + + } + @EventListener public void tooManyParameters(String event, String whatIsThis) { } @@ -313,6 +360,19 @@ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEv } + private static class EntityWrapper implements ResolvableTypeProvider { + private final T entity; + + public EntityWrapper(T entity) { + this.entity = entity; + } + + @Override + public ResolvableType getResolvableType() { + return ResolvableType.forClassWithGenerics(getClass(), this.entity.getClass()); + } + } + static class InvalidProxyTestBean implements SimpleService { @Override 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 index 20243c8eb8..c694c181da 100644 --- a/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java @@ -66,6 +66,12 @@ public class GenericApplicationListenerAdapterTests extends AbstractApplicationE supportsEventType(true, StringEventListener.class, eventType); } + @Test // or if the event provides its precise type + public void genericListenerStrictTypeAndResolvableTypeProvider() { + ResolvableType eventType = new SmartGenericTestEvent<>(this, "foo").getResolvableType(); + supportsEventType(true, StringEventListener.class, eventType); + } + @Test // Demonstrates it works if we actually use the subtype public void genericListenerStrictTypeEventSubType() { StringEvent stringEvent = new StringEvent(this, "test"); diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index 1dd9dee7ca..35b88c63ab 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -67,6 +67,7 @@ import org.springframework.util.StringUtils; * * @author Phillip Webb * @author Juergen Hoeller + * @author Stephane Nicoll * @since 4.0 * @see #forField(Field) * @see #forMethodParameter(Method, int) @@ -74,6 +75,8 @@ import org.springframework.util.StringUtils; * @see #forConstructorParameter(Constructor, int) * @see #forClass(Class) * @see #forType(Type) + * @see #forInstance(Object) + * @see ResolvableTypeProvider */ @SuppressWarnings("serial") public class ResolvableType implements Serializable { @@ -984,6 +987,26 @@ public class ResolvableType implements Serializable { return forType(syntheticType, new TypeVariablesVariableResolver(variables, generics)); } + /** + * Return a {@link ResolvableType} for the specified instance. The instance does not + * convey generic information but if it implements {@link ResolvableTypeProvider} a + * more precise {@link ResolvableType} can be used than the simple one based on + * the {@link #forClass(Class) Class instance}. + * @param instance the instance + * @return a {@link ResolvableType} for the specified instance + * @see ResolvableTypeProvider + */ + public static ResolvableType forInstance(Object instance) { + Assert.notNull(instance, "Instance must not be null"); + if (instance instanceof ResolvableTypeProvider) { + ResolvableType type = ((ResolvableTypeProvider) instance).getResolvableType(); + if (type != null) { + return type; + } + } + return ResolvableType.forClass(instance.getClass()); + } + /** * Return a {@link ResolvableType} for the specified {@link Field}. * @param field the source field diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableTypeProvider.java b/spring-core/src/main/java/org/springframework/core/ResolvableTypeProvider.java new file mode 100644 index 0000000000..dc0490ade9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ResolvableTypeProvider.java @@ -0,0 +1,40 @@ +/* + * 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.core; + +/** + * Any object can implement this interface to provide its actual {@link ResolvableType}. + *

+ * Such information is very useful when figuring out if the instance matches a generic + * signature as Java does not convey the signature at runtime. + *

+ * Users of this interface should be careful in complex hierarchy scenarios, especially + * when the generic type signature of the class changes in sub-classes. It is always + * possible to return {@code null} to fallback on a default behaviour. + * + * @author Stephane Nicoll + * @since 4.2 + */ +public interface ResolvableTypeProvider { + + /** + * Return the {@link ResolvableType} describing this instance or {@code null} if some + * sort of default should be applied instead. + */ + ResolvableType getResolvableType(); + +} diff --git a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java index d24cceb88d..5c90f5cba9 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -133,6 +133,34 @@ public class ResolvableTypeTests { assertTrue(type.isAssignableFrom(String.class)); } + @Test + public void forInstanceMustNotBeNull() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Instance must not be null"); + ResolvableType.forInstance(null); + } + + @Test + public void forInstanceNoProvider() { + ResolvableType type = ResolvableType.forInstance(new Object()); + assertThat(type.getType(), equalTo(Object.class)); + assertThat(type.resolve(), equalTo(Object.class)); + } + + @Test + public void forInstanceProvider() { + ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType(String.class)); + assertThat(type.getRawClass(), equalTo(MyGenericInterfaceType.class)); + assertThat(type.getGeneric().resolve(), equalTo(String.class)); + } + + @Test + public void forInstanceProviderNull() { + ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType(null)); + assertThat(type.getType(), equalTo(MyGenericInterfaceType.class)); + assertThat(type.resolve(), equalTo(MyGenericInterfaceType.class)); + } + @Test public void forField() throws Exception { Field field = Fields.class.getField("charSequenceList"); @@ -1454,6 +1482,23 @@ public class ResolvableTypeTests { public interface MyInterfaceType { } + public class MyGenericInterfaceType implements MyInterfaceType, ResolvableTypeProvider { + + private final Class type; + + public MyGenericInterfaceType(Class type) { + this.type = type; + } + + @Override + public ResolvableType getResolvableType() { + if (this.type == null) { + return null; + } + return ResolvableType.forClassWithGenerics(getClass(), this.type); + } + } + public class MySimpleInterfaceType implements MyInterfaceType { }