Skip to content

Commit 1e4f2ea

Browse files
committed
Fix generic with WildcardType return type support in HttpServiceMethod
Signed-off-by: anaconda875 <hflbtmax@gmail.com>
1 parent 18a8995 commit 1e4f2ea

4 files changed

Lines changed: 211 additions & 15 deletions

File tree

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

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,8 @@ public static Class<?> resolveReturnType(Method method, Class<?> clazz) {
154154
public static Type resolveType(Type genericType, @Nullable Class<?> contextClass) {
155155
if (contextClass != null) {
156156
if (genericType instanceof TypeVariable<?> typeVariable) {
157-
ResolvableType resolvedTypeVariable = resolveVariable(
157+
ResolvableType resolvedTypeVariable = resolveVariableConsiderBound(
158158
typeVariable, ResolvableType.forClass(contextClass));
159-
if (resolvedTypeVariable == ResolvableType.NONE) {
160-
resolvedTypeVariable = ResolvableType.forVariableBounds(typeVariable);
161-
}
162159
if (resolvedTypeVariable != ResolvableType.NONE) {
163160
Type type = resolvedTypeVariable.getType();
164161
if (type instanceof ParameterizedType) {
@@ -179,18 +176,16 @@ else if (genericType instanceof ParameterizedType parameterizedType) {
179176
for (int i = 0; i < typeArguments.length; i++) {
180177
Type typeArgument = typeArguments[i];
181178
if (typeArgument instanceof TypeVariable<?> typeVariable) {
182-
ResolvableType resolvedTypeArgument = resolveVariable(typeVariable, contextType);
183-
if (resolvedTypeArgument == ResolvableType.NONE) {
184-
resolvedTypeArgument = ResolvableType.forVariableBounds(typeVariable);
185-
}
179+
ResolvableType resolvedTypeArgument = resolveVariableConsiderBound(
180+
typeVariable, contextType);
186181
if (resolvedTypeArgument != ResolvableType.NONE) {
187182
generics[i] = resolvedTypeArgument;
188183
}
189184
else {
190185
generics[i] = ResolvableType.forType(typeArgument);
191186
}
192187
}
193-
else if (typeArgument instanceof ParameterizedType) {
188+
else if (typeArgument instanceof ParameterizedType || typeArgument instanceof WildcardType) {
194189
generics[i] = ResolvableType.forType(resolveType(typeArgument, contextClass));
195190
}
196191
else {
@@ -203,10 +198,39 @@ else if (typeArgument instanceof ParameterizedType) {
203198
}
204199
}
205200
}
201+
else if (genericType instanceof WildcardType wildcardType) {
202+
Type[] originalLowerBound = wildcardType.getLowerBounds();
203+
Type[] originalUpperBound = wildcardType.getUpperBounds();
204+
205+
if (originalLowerBound.length == 1) {
206+
Type lowerBound = resolveType(originalLowerBound[0], contextClass);
207+
if (lowerBound != originalLowerBound[0]) {
208+
return ResolvableType.forWildCardTypeWithLowerBound(
209+
wildcardType, ResolvableType.forType(lowerBound))
210+
.getType();
211+
}
212+
} else if (originalUpperBound.length == 1) {
213+
Type upperBound = resolveType(originalUpperBound[0], contextClass);
214+
if (upperBound != originalUpperBound[0]) {
215+
return ResolvableType.forWildCardTypeWithUpperBound(
216+
wildcardType, ResolvableType.forType(upperBound))
217+
.getType();
218+
}
219+
}
220+
return wildcardType;
221+
}
206222
}
207223
return genericType;
208224
}
209225

226+
private static ResolvableType resolveVariableConsiderBound(TypeVariable<?> typeVariable, ResolvableType contextType) {
227+
ResolvableType resolvedTypeArgument = resolveVariable(typeVariable, contextType);
228+
if (resolvedTypeArgument == ResolvableType.NONE) {
229+
resolvedTypeArgument = ResolvableType.forVariableBounds(typeVariable);
230+
}
231+
return resolvedTypeArgument;
232+
}
233+
210234
private static ResolvableType resolveVariable(TypeVariable<?> typeVariable, ResolvableType contextType) {
211235
ResolvableType resolvedType;
212236
if (contextType.hasGenerics()) {

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

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ public class ResolvableType implements Serializable {
9999
private static final ConcurrentReferenceHashMap<ResolvableType, ResolvableType> cache =
100100
new ConcurrentReferenceHashMap<>(256);
101101

102+
private static final Type[] EMPTY_TYPE_ARRAY = new Type[0];
103+
102104

103105
/**
104106
* The underlying Java type being managed.
@@ -617,7 +619,8 @@ private boolean determineUnresolvableGenerics(@Nullable Set<Type> alreadySeen) {
617619

618620
ResolvableType[] generics = getGenerics();
619621
for (ResolvableType generic : generics) {
620-
if (generic.isUnresolvableTypeVariable() || generic.isWildcardWithoutBounds() ||
622+
if (generic.isUnresolvableTypeVariable() ||
623+
generic.isUnresolvableWildcard(currentTypeSeen(alreadySeen)) ||
621624
generic.hasUnresolvableGenerics(currentTypeSeen(alreadySeen))) {
622625
return true;
623626
}
@@ -677,14 +680,32 @@ private boolean isWildcardWithoutBounds() {
677680
if (this.type instanceof WildcardType wildcardType) {
678681
if (wildcardType.getLowerBounds().length == 0) {
679682
Type[] upperBounds = wildcardType.getUpperBounds();
680-
if (upperBounds.length == 0 || (upperBounds.length == 1 && Object.class == upperBounds[0])) {
681-
return true;
682-
}
683+
return upperBounds.length == 0 || (upperBounds.length == 1 && (Object.class == upperBounds[0]));
683684
}
684685
}
685686
return false;
686687
}
687688

689+
/**
690+
* Determine whether the underlying type represents a wildcard
691+
* has unresolvable upper bound or lower bound, or simply without bound
692+
*/
693+
private boolean isUnresolvableWildcard(Set<Type> alreadySeen) {
694+
if (this.type instanceof WildcardType wildcardType) {
695+
Type[] lowerBounds = wildcardType.getLowerBounds();
696+
if (lowerBounds.length == 1) {
697+
ResolvableType lowerResolvable = ResolvableType.forType(lowerBounds[0], this.variableResolver);
698+
return lowerResolvable.isUnresolvableTypeVariable() || lowerResolvable.determineUnresolvableGenerics(alreadySeen);
699+
}
700+
Type[] upperBounds = wildcardType.getUpperBounds();
701+
if (upperBounds.length == 1 && upperBounds[0] != Object.class) {
702+
ResolvableType upperResolvable = ResolvableType.forType(upperBounds[0], this.variableResolver);
703+
return upperResolvable.isUnresolvableTypeVariable() || upperResolvable.determineUnresolvableGenerics(alreadySeen);
704+
}
705+
}
706+
return isWildcardWithoutBounds();
707+
}
708+
688709
/**
689710
* Return a {@code ResolvableType} for the specified nesting level.
690711
* <p>See {@link #getNested(int, Map)} for details.
@@ -1185,6 +1206,51 @@ public static ResolvableType forClassWithGenerics(Class<?> clazz, @Nullable Reso
11851206
(generics != null ? new TypeVariablesVariableResolver(variables, generics) : null));
11861207
}
11871208

1209+
/**
1210+
* Return a {@code ResolvableType} for the specified {@link WildcardType} with pre-declared upper bound.
1211+
* @param wildcardType the WildcardType to introspect
1212+
* @param upperBound the upper bound of the wildcardType
1213+
* @return a {@code ResolvableType} for the specific wildcardType and upperBound
1214+
*/
1215+
public static ResolvableType forWildCardTypeWithUpperBound(WildcardType wildcardType, ResolvableType upperBound) {
1216+
Assert.notNull(wildcardType, "WildcardType must not be null");
1217+
Assert.notNull(upperBound, "UpperBound must not be null");
1218+
Type[] originalLowerBound = wildcardType.getLowerBounds();
1219+
Assert.isTrue(originalLowerBound.length == 0,
1220+
() -> "The WildcardType has lower bound while upper bound provided " + wildcardType);
1221+
1222+
Type upperBoundType = upperBound.getType();
1223+
VariableResolver variableResolver = upperBoundType instanceof TypeVariable<?> typeVariable
1224+
? new TypeVariablesVariableResolver(
1225+
new TypeVariable<?>[]{typeVariable}, new ResolvableType[]{upperBound})
1226+
: null;
1227+
1228+
return forType(new WildcardTypeImpl(new Type[]{upperBoundType}, EMPTY_TYPE_ARRAY), variableResolver);
1229+
}
1230+
1231+
/**
1232+
* Return a {@code ResolvableType} for the specified {@link WildcardType} with pre-declared lower bound.
1233+
* @param wildcardType the WildcardType to introspect
1234+
* @param lowerBound the lower bound of the wildcardType
1235+
* @return a {@code ResolvableType} for the specific wildcardType and lowerBound
1236+
*/
1237+
public static ResolvableType forWildCardTypeWithLowerBound(WildcardType wildcardType, ResolvableType lowerBound) {
1238+
Assert.notNull(wildcardType, "WildcardType must not be null");
1239+
Assert.notNull(lowerBound, "LowerBound must not be null");
1240+
Type[] originalUpperBound = wildcardType.getUpperBounds();
1241+
Assert.isTrue(originalUpperBound.length == 0 || originalUpperBound[0] == Object.class,
1242+
() -> "The WildcardType has upper bound %s while lower bound provided %s"
1243+
.formatted(originalUpperBound[0], wildcardType));
1244+
1245+
Type lowerBoundType = lowerBound.getType();
1246+
VariableResolver variableResolver = lowerBoundType instanceof TypeVariable<?> typeVariable
1247+
? new TypeVariablesVariableResolver(
1248+
new TypeVariable<?>[]{typeVariable}, new ResolvableType[]{lowerBound})
1249+
: null;
1250+
1251+
return forType(new WildcardTypeImpl(new Type[]{Object.class}, new Type[]{lowerBoundType}), variableResolver);
1252+
}
1253+
11881254
/**
11891255
* Return a {@code ResolvableType} for the specified instance. The instance does not
11901256
* convey generic information but if it implements {@link ResolvableTypeProvider} a
@@ -1634,6 +1700,56 @@ public Object getSource() {
16341700
}
16351701

16361702

1703+
private static final class WildcardTypeImpl implements WildcardType, Serializable {
1704+
1705+
private final Type[] upperBound;
1706+
private final Type[] lowerBound;
1707+
1708+
private WildcardTypeImpl(Type[] upperBound, Type[] lowerBound) {
1709+
this.upperBound = upperBound;
1710+
this.lowerBound = lowerBound;
1711+
}
1712+
1713+
@Override
1714+
public Type[] getUpperBounds() {
1715+
return upperBound.clone();
1716+
}
1717+
1718+
@Override
1719+
public Type[] getLowerBounds() {
1720+
return lowerBound.clone();
1721+
}
1722+
1723+
@Override
1724+
public boolean equals(Object o) {
1725+
if (!(o instanceof WildcardType that)) {
1726+
return false;
1727+
}
1728+
return Arrays.equals(upperBound, that.getUpperBounds()) && Arrays.equals(lowerBound, that.getLowerBounds());
1729+
}
1730+
1731+
@Override
1732+
public int hashCode() {
1733+
return Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds());
1734+
}
1735+
1736+
@Override
1737+
public String toString() {
1738+
if (getLowerBounds().length == 1) {
1739+
return "? super " + typeToString(getLowerBounds()[0]);
1740+
}
1741+
if (getUpperBounds().length == 0 || getUpperBounds()[0] == Object.class) {
1742+
return "?";
1743+
}
1744+
return "? extends " + typeToString(getUpperBounds()[0]);
1745+
}
1746+
1747+
private static String typeToString(Type type) {
1748+
return type instanceof Class<?> cls ? cls.getName() : type.toString();
1749+
}
1750+
}
1751+
1752+
16371753
private static final class SyntheticParameterizedType implements ParameterizedType, Serializable {
16381754

16391755
private final Type rawType;

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@
2222
import java.lang.reflect.Type;
2323
import java.lang.reflect.TypeVariable;
2424
import java.util.Collection;
25+
import java.util.Collections;
2526
import java.util.HashMap;
2627
import java.util.List;
2728
import java.util.Map;
2829
import java.util.Optional;
2930
import java.util.function.Supplier;
3031

3132
import org.junit.jupiter.api.Test;
33+
import org.junit.jupiter.params.ParameterizedTest;
34+
import org.junit.jupiter.params.provider.ValueSource;
3235

3336
import static org.assertj.core.api.Assertions.assertThat;
3437
import static org.springframework.core.GenericTypeResolver.getTypeVariableMap;
@@ -251,6 +254,14 @@ void resolveTypeFromGenericDefaultMethod() {
251254
assertThat(resolvedType).isEqualTo(InheritsDefaultMethod.ConcreteType.class);
252255
}
253256

257+
@ParameterizedTest
258+
@ValueSource(strings = {"getUpperBound", "getLowerBound"})
259+
void resolveTypeFromWildcardType(String methodName) {
260+
Type type = method(MyInterfaceType.class, methodName).getGenericReturnType();
261+
Type resolvedType = resolveType(type, MySimpleInterfaceType.class);
262+
assertThat(resolvedType).isEqualTo(method(MySimpleInterfaceType.class, methodName).getGenericReturnType());
263+
}
264+
254265
@Test
255266
void resolveTypeFromNestedParameterizedType() {
256267
Type resolvedType = resolveType(method(MyInterfaceType.class, "get").getGenericReturnType(), MyCollectionInterfaceType.class);
@@ -268,12 +279,29 @@ private static Method method(Class<?> target, String methodName, Class<?>... par
268279

269280

270281
public interface MyInterfaceType<T> {
282+
default Optional<? extends T> getUpperBound() {
283+
return Optional.empty();
284+
}
285+
286+
default List<? super T> getLowerBound() {
287+
return Collections.emptyList();
288+
}
289+
271290
default T get() {
272291
return null;
273292
}
274293
}
275294

276295
public class MySimpleInterfaceType implements MyInterfaceType<String> {
296+
@Override
297+
public Optional<? extends String> getUpperBound() {
298+
return MyInterfaceType.super.getUpperBound();
299+
}
300+
301+
@Override
302+
public List<? super String> getLowerBound() {
303+
return MyInterfaceType.super.getLowerBound();
304+
}
277305
}
278306

279307
public class MyParameterizedInterfaceType<P> implements MyInterfaceType<Collection<P>> {

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,13 @@
4242
import java.util.SortedSet;
4343
import java.util.TreeSet;
4444
import java.util.concurrent.Callable;
45+
import java.util.stream.Stream;
4546

4647
import org.assertj.core.api.AbstractAssert;
4748
import org.junit.jupiter.api.Test;
4849
import org.junit.jupiter.api.extension.ExtendWith;
50+
import org.junit.jupiter.params.ParameterizedTest;
51+
import org.junit.jupiter.params.provider.MethodSource;
4952
import org.mockito.ArgumentCaptor;
5053
import org.mockito.Captor;
5154
import org.mockito.junit.jupiter.MockitoExtension;
@@ -1601,6 +1604,31 @@ void gh34541() throws Exception {
16011604
assertThat(typeWithGenerics.isAssignableFrom(PaymentCreator.class)).isTrue();
16021605
}
16031606

1607+
@ParameterizedTest
1608+
@MethodSource("wildcardInfo")
1609+
void gh36474(ResolvableType typeVariable, Class<?> resolved) {
1610+
assertThat(typeVariable.resolve()).isEqualTo(resolved);
1611+
}
1612+
1613+
1614+
static Stream<Object[]> wildcardInfo() throws Exception {
1615+
WildcardType listxs = getWildcardType(AssignmentBase.class, "listxs");
1616+
WildcardType listsc = getWildcardType(AssignmentBase.class, "listsc");
1617+
ResolvableType owner = ResolvableType.forType(Assignment.class).as(AssignmentBase.class);
1618+
1619+
ResolvableType lbWildcard = ResolvableType.forWildCardTypeWithUpperBound(
1620+
listxs, ResolvableType.forType(listxs.getUpperBounds()[0], owner));
1621+
ResolvableType ubWildcard = ResolvableType.forWildCardTypeWithLowerBound(
1622+
listsc, ResolvableType.forType(listsc.getLowerBounds()[0], owner));
1623+
return Stream.of(new Object[] {lbWildcard, String.class}, new Object[] {ubWildcard, CharSequence.class});
1624+
}
1625+
1626+
1627+
static WildcardType getWildcardType(Class<?> cls, String field) throws Exception {
1628+
ResolvableType type = ResolvableType.forField(cls.getField(field));
1629+
return (WildcardType) type.getGeneric(0).getType();
1630+
}
1631+
16041632

16051633
private ResolvableType testSerialization(ResolvableType type) throws Exception {
16061634
ByteArrayOutputStream bos = new ByteArrayOutputStream();
@@ -1631,13 +1659,13 @@ private static ResolvableTypeAssert assertThatResolvableType(ResolvableType type
16311659
@SuppressWarnings("unused")
16321660
private HashMap<Integer, List<String>> myMap;
16331661

1634-
16351662
@SuppressWarnings("serial")
16361663
static class ExtendsList extends ArrayList<CharSequence> {
1637-
}
16381664

1665+
}
16391666
@SuppressWarnings("serial")
16401667
static class ExtendsMap extends HashMap<String, Integer> {
1668+
16411669
}
16421670

16431671

0 commit comments

Comments
 (0)