Skip to content

Commit b87816e

Browse files
committed
Add ResolvableTypeProvider
Provide a mean to detect the actual ResolvableType based on a instance as a counter measure to type erasure. Upgrade the event infrastructure to detect if the event (or the payload) implements such interface. When this is the case, the return value of `getResolvableType` is used to validate its generic type against the method signature of the listener. Issue: SPR-13069
1 parent a7aaf31 commit b87816e

File tree

10 files changed

+217
-7
lines changed

10 files changed

+217
-7
lines changed

spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.context;
1818

19+
import org.springframework.core.ResolvableType;
20+
import org.springframework.core.ResolvableTypeProvider;
1921
import org.springframework.util.Assert;
2022

2123
/**
@@ -28,7 +30,7 @@
2830
* @param <T> the payload type of the event
2931
*/
3032
@SuppressWarnings("serial")
31-
public class PayloadApplicationEvent<T> extends ApplicationEvent {
33+
public class PayloadApplicationEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {
3234

3335
private final T payload;
3436

@@ -44,6 +46,11 @@ public PayloadApplicationEvent(Object source, T payload) {
4446
this.payload = payload;
4547
}
4648

49+
@Override
50+
public ResolvableType getResolvableType() {
51+
return ResolvableType.forClassWithGenerics(getClass(),
52+
ResolvableType.forInstance(getPayload()));
53+
}
4754

4855
/**
4956
* Return the payload of the event.

spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,11 @@ public void processEvent(ApplicationEvent event) {
124124
protected Object[] resolveArguments(ApplicationEvent event) {
125125
if (!ApplicationEvent.class.isAssignableFrom(this.declaredEventType.getRawClass())
126126
&& event instanceof PayloadApplicationEvent) {
127-
@SuppressWarnings("rawtypes")
128-
Object payload = ((PayloadApplicationEvent) event).getPayload();
129-
if (this.declaredEventType.isAssignableFrom(ResolvableType.forClass(payload.getClass()))) {
130-
return new Object[] {payload};
127+
PayloadApplicationEvent<?> payloadEvent = (PayloadApplicationEvent<?>) event;
128+
ResolvableType payloadType = payloadEvent.getResolvableType()
129+
.as(PayloadApplicationEvent.class).getGeneric(0);
130+
if (this.declaredEventType.isAssignableFrom(payloadType)) {
131+
return new Object[] {payloadEvent.getPayload()};
131132
}
132133
}
133134
else {

spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public void run() {
139139
}
140140

141141
private ResolvableType resolveDefaultEventType(ApplicationEvent event) {
142-
return ResolvableType.forType(event.getClass());
142+
return ResolvableType.forInstance(event);
143143
}
144144

145145
/**

spring-context/src/test/java/org/springframework/context/event/AbstractApplicationEventListenerTests.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.context.ApplicationEvent;
2222
import org.springframework.context.ApplicationListener;
2323
import org.springframework.core.ResolvableType;
24+
import org.springframework.core.ResolvableTypeProvider;
2425

2526
/**
2627
* @author Stephane Nicoll
@@ -53,6 +54,23 @@ public T getPayload() {
5354

5455
}
5556

57+
protected static class SmartGenericTestEvent<T>
58+
extends GenericTestEvent<T> implements ResolvableTypeProvider {
59+
60+
private final ResolvableType resolvableType;
61+
62+
public SmartGenericTestEvent(Object source, T payload) {
63+
super(source, payload);
64+
this.resolvableType = ResolvableType.forClassWithGenerics(
65+
getClass(), payload.getClass());
66+
}
67+
68+
@Override
69+
public ResolvableType getResolvableType() {
70+
return this.resolvableType;
71+
}
72+
}
73+
5674
protected static class StringEvent extends GenericTestEvent<String> {
5775

5876
public StringEvent(Object source, String payload) {

spring-context/src/test/java/org/springframework/context/event/ApplicationContextEventTests.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public void multicastGenericEventWrongType() {
7575
getGenericApplicationEventType("longEvent"));
7676
}
7777

78-
@Test // Unfortunate - this should work as well
78+
@Test
7979
public void multicastGenericEventWildcardSubType() {
8080
multicastEvent(false, StringEventListener.class, createGenericTestEvent("test"),
8181
getGenericApplicationEventType("wildcardEvent"));
@@ -91,6 +91,16 @@ public void multicastConcreteWrongTypeGenericListener() {
9191
multicastEvent(false, StringEventListener.class, new LongEvent(this, 123L), null);
9292
}
9393

94+
@Test
95+
public void multicastSmartGenericTypeGenericListener() {
96+
multicastEvent(true, StringEventListener.class, new SmartGenericTestEvent<>(this, "test"), null);
97+
}
98+
99+
@Test
100+
public void multicastSmartGenericWrongTypeGenericListener() {
101+
multicastEvent(false, StringEventListener.class, new SmartGenericTestEvent<>(this, 123L), null);
102+
}
103+
94104
private void multicastEvent(boolean match, Class<?> listenerType,
95105
ApplicationEvent event, ResolvableType eventType) {
96106
@SuppressWarnings("unchecked")

spring-context/src/test/java/org/springframework/context/event/ApplicationListenerMethodAdapterTests.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.context.ApplicationEvent;
3030
import org.springframework.context.PayloadApplicationEvent;
3131
import org.springframework.core.ResolvableType;
32+
import org.springframework.core.ResolvableTypeProvider;
3233
import org.springframework.core.annotation.Order;
3334
import org.springframework.util.ReflectionUtils;
3435

@@ -155,6 +156,42 @@ public void invokeListener() {
155156
verify(this.sampleEvents, times(1)).handleGenericString(event);
156157
}
157158

159+
@Test
160+
public void invokeListenerWithGenericEvent() {
161+
Method method = ReflectionUtils.findMethod(SampleEvents.class,
162+
"handleGenericString", GenericTestEvent.class);
163+
GenericTestEvent<String> event = new SmartGenericTestEvent<>(this, "test");
164+
invokeListener(method, event);
165+
verify(this.sampleEvents, times(1)).handleGenericString(event);
166+
}
167+
168+
@Test
169+
public void invokeListenerWithGenericPayload() {
170+
Method method = ReflectionUtils.findMethod(SampleEvents.class,
171+
"handleGenericStringPayload", EntityWrapper.class);
172+
EntityWrapper<String> payload = new EntityWrapper<>("test");
173+
invokeListener(method, new PayloadApplicationEvent<>(this, payload));
174+
verify(this.sampleEvents, times(1)).handleGenericStringPayload(payload);
175+
}
176+
177+
@Test
178+
public void invokeListenerWithWrongGenericPayload() {
179+
Method method = ReflectionUtils.findMethod(SampleEvents.class,
180+
"handleGenericStringPayload", EntityWrapper.class);
181+
EntityWrapper<Integer> payload = new EntityWrapper<>(123);
182+
invokeListener(method, new PayloadApplicationEvent<>(this, payload));
183+
verify(this.sampleEvents, times(0)).handleGenericStringPayload(any());
184+
}
185+
186+
@Test
187+
public void invokeListenerWithAnyGenericPayload() {
188+
Method method = ReflectionUtils.findMethod(SampleEvents.class,
189+
"handleGenericAnyPayload", EntityWrapper.class);
190+
EntityWrapper<String> payload = new EntityWrapper<>("test");
191+
invokeListener(method, new PayloadApplicationEvent<>(this, payload));
192+
verify(this.sampleEvents, times(1)).handleGenericAnyPayload(payload);
193+
}
194+
158195
@Test
159196
public void invokeListenerRuntimeException() {
160197
Method method = ReflectionUtils.findMethod(SampleEvents.class,
@@ -284,6 +321,16 @@ public void handleGenericString(GenericTestEvent<String> event) {
284321
public void handleString(String payload) {
285322
}
286323

324+
@EventListener
325+
public void handleGenericStringPayload(EntityWrapper<String> event) {
326+
327+
}
328+
329+
@EventListener
330+
public void handleGenericAnyPayload(EntityWrapper<?> event) {
331+
332+
}
333+
287334
@EventListener
288335
public void tooManyParameters(String event, String whatIsThis) {
289336
}
@@ -313,6 +360,19 @@ interface SimpleService {
313360

314361
}
315362

363+
private static class EntityWrapper<T> implements ResolvableTypeProvider {
364+
private final T entity;
365+
366+
public EntityWrapper(T entity) {
367+
this.entity = entity;
368+
}
369+
370+
@Override
371+
public ResolvableType getResolvableType() {
372+
return ResolvableType.forClassWithGenerics(getClass(), this.entity.getClass());
373+
}
374+
}
375+
316376
static class InvalidProxyTestBean implements SimpleService {
317377

318378
@Override

spring-context/src/test/java/org/springframework/context/event/GenericApplicationListenerAdapterTests.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ public void genericListenerStrictTypeAndResolvableType() {
6666
supportsEventType(true, StringEventListener.class, eventType);
6767
}
6868

69+
@Test // or if the event provides its precise type
70+
public void genericListenerStrictTypeAndResolvableTypeProvider() {
71+
ResolvableType eventType = new SmartGenericTestEvent<>(this, "foo").getResolvableType();
72+
supportsEventType(true, StringEventListener.class, eventType);
73+
}
74+
6975
@Test // Demonstrates it works if we actually use the subtype
7076
public void genericListenerStrictTypeEventSubType() {
7177
StringEvent stringEvent = new StringEvent(this, "test");

spring-core/src/main/java/org/springframework/core/ResolvableType.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,16 @@
6767
*
6868
* @author Phillip Webb
6969
* @author Juergen Hoeller
70+
* @author Stephane Nicoll
7071
* @since 4.0
7172
* @see #forField(Field)
7273
* @see #forMethodParameter(Method, int)
7374
* @see #forMethodReturnType(Method)
7475
* @see #forConstructorParameter(Constructor, int)
7576
* @see #forClass(Class)
7677
* @see #forType(Type)
78+
* @see #forInstance(Object)
79+
* @see ResolvableTypeProvider
7780
*/
7881
@SuppressWarnings("serial")
7982
public class ResolvableType implements Serializable {
@@ -984,6 +987,26 @@ public static ResolvableType forClassWithGenerics(Class<?> sourceClass, Resolvab
984987
return forType(syntheticType, new TypeVariablesVariableResolver(variables, generics));
985988
}
986989

990+
/**
991+
* Return a {@link ResolvableType} for the specified instance. The instance does not
992+
* convey generic information but if it implements {@link ResolvableTypeProvider} a
993+
* more precise {@link ResolvableType} can be used than the simple one based on
994+
* the {@link #forClass(Class) Class instance}.
995+
* @param instance the instance
996+
* @return a {@link ResolvableType} for the specified instance
997+
* @see ResolvableTypeProvider
998+
*/
999+
public static ResolvableType forInstance(Object instance) {
1000+
Assert.notNull(instance, "Instance must not be null");
1001+
if (instance instanceof ResolvableTypeProvider) {
1002+
ResolvableType type = ((ResolvableTypeProvider) instance).getResolvableType();
1003+
if (type != null) {
1004+
return type;
1005+
}
1006+
}
1007+
return ResolvableType.forClass(instance.getClass());
1008+
}
1009+
9871010
/**
9881011
* Return a {@link ResolvableType} for the specified {@link Field}.
9891012
* @param field the source field
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2002-2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.core;
18+
19+
/**
20+
* Any object can implement this interface to provide its actual {@link ResolvableType}.
21+
* <p>
22+
* Such information is very useful when figuring out if the instance matches a generic
23+
* signature as Java does not convey the signature at runtime.
24+
* <p>
25+
* Users of this interface should be careful in complex hierarchy scenarios, especially
26+
* when the generic type signature of the class changes in sub-classes. It is always
27+
* possible to return {@code null} to fallback on a default behaviour.
28+
*
29+
* @author Stephane Nicoll
30+
* @since 4.2
31+
*/
32+
public interface ResolvableTypeProvider {
33+
34+
/**
35+
* Return the {@link ResolvableType} describing this instance or {@code null} if some
36+
* sort of default should be applied instead.
37+
*/
38+
ResolvableType getResolvableType();
39+
40+
}

spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,34 @@ public void forRawClassWithNull() throws Exception {
133133
assertTrue(type.isAssignableFrom(String.class));
134134
}
135135

136+
@Test
137+
public void forInstanceMustNotBeNull() {
138+
this.thrown.expect(IllegalArgumentException.class);
139+
this.thrown.expectMessage("Instance must not be null");
140+
ResolvableType.forInstance(null);
141+
}
142+
143+
@Test
144+
public void forInstanceNoProvider() {
145+
ResolvableType type = ResolvableType.forInstance(new Object());
146+
assertThat(type.getType(), equalTo(Object.class));
147+
assertThat(type.resolve(), equalTo(Object.class));
148+
}
149+
150+
@Test
151+
public void forInstanceProvider() {
152+
ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType<String>(String.class));
153+
assertThat(type.getRawClass(), equalTo(MyGenericInterfaceType.class));
154+
assertThat(type.getGeneric().resolve(), equalTo(String.class));
155+
}
156+
157+
@Test
158+
public void forInstanceProviderNull() {
159+
ResolvableType type = ResolvableType.forInstance(new MyGenericInterfaceType<String>(null));
160+
assertThat(type.getType(), equalTo(MyGenericInterfaceType.class));
161+
assertThat(type.resolve(), equalTo(MyGenericInterfaceType.class));
162+
}
163+
136164
@Test
137165
public void forField() throws Exception {
138166
Field field = Fields.class.getField("charSequenceList");
@@ -1454,6 +1482,23 @@ public TypedConstructors(Map<String, Long> p) {
14541482
public interface MyInterfaceType<T> {
14551483
}
14561484

1485+
public class MyGenericInterfaceType<T> implements MyInterfaceType<T>, ResolvableTypeProvider {
1486+
1487+
private final Class<T> type;
1488+
1489+
public MyGenericInterfaceType(Class<T> type) {
1490+
this.type = type;
1491+
}
1492+
1493+
@Override
1494+
public ResolvableType getResolvableType() {
1495+
if (this.type == null) {
1496+
return null;
1497+
}
1498+
return ResolvableType.forClassWithGenerics(getClass(), this.type);
1499+
}
1500+
}
1501+
14571502

14581503
public class MySimpleInterfaceType implements MyInterfaceType<String> {
14591504
}

0 commit comments

Comments
 (0)