From 018487e2ba464b328487961beb5015cb0f5534b2 Mon Sep 17 00:00:00 2001 From: anaconda875 Date: Mon, 16 Mar 2026 15:41:30 +0700 Subject: [PATCH] Fix generic with WildcardType return type support in HttpServiceMethod Signed-off-by: anaconda875 --- .../core/GenericTypeResolver.java | 42 ++++-- .../springframework/core/ResolvableType.java | 124 +++++++++++++++++- .../core/GenericTypeResolverTests.java | 28 ++++ .../core/ResolvableTypeTests.java | 32 ++++- 4 files changed, 211 insertions(+), 15 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java index 6ea0eae822c..18cb9a7a27c 100644 --- a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java +++ b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java @@ -154,11 +154,8 @@ public final class GenericTypeResolver { public static Type resolveType(Type genericType, @Nullable Class contextClass) { if (contextClass != null) { if (genericType instanceof TypeVariable typeVariable) { - ResolvableType resolvedTypeVariable = resolveVariable( + ResolvableType resolvedTypeVariable = resolveVariableConsiderBound( typeVariable, ResolvableType.forClass(contextClass)); - if (resolvedTypeVariable == ResolvableType.NONE) { - resolvedTypeVariable = ResolvableType.forVariableBounds(typeVariable); - } if (resolvedTypeVariable != ResolvableType.NONE) { Class resolved = resolvedTypeVariable.resolve(); if (resolved != null) { @@ -175,10 +172,8 @@ public final class GenericTypeResolver { for (int i = 0; i < typeArguments.length; i++) { Type typeArgument = typeArguments[i]; if (typeArgument instanceof TypeVariable typeVariable) { - ResolvableType resolvedTypeArgument = resolveVariable(typeVariable, contextType); - if (resolvedTypeArgument == ResolvableType.NONE) { - resolvedTypeArgument = ResolvableType.forVariableBounds(typeVariable); - } + ResolvableType resolvedTypeArgument = resolveVariableConsiderBound( + typeVariable, contextType); if (resolvedTypeArgument != ResolvableType.NONE) { generics[i] = resolvedTypeArgument; } @@ -186,7 +181,7 @@ public final class GenericTypeResolver { generics[i] = ResolvableType.forType(typeArgument); } } - else if (typeArgument instanceof ParameterizedType) { + else if (typeArgument instanceof ParameterizedType || typeArgument instanceof WildcardType) { generics[i] = ResolvableType.forType(resolveType(typeArgument, contextClass)); } else { @@ -199,10 +194,39 @@ public final class GenericTypeResolver { } } } + else if (genericType instanceof WildcardType wildcardType) { + Type[] originalLowerBound = wildcardType.getLowerBounds(); + Type[] originalUpperBound = wildcardType.getUpperBounds(); + + if (originalLowerBound.length == 1) { + Type lowerBound = resolveType(originalLowerBound[0], contextClass); + if (lowerBound != originalLowerBound[0]) { + return ResolvableType.forWildCardTypeWithLowerBound( + wildcardType, ResolvableType.forType(lowerBound)) + .getType(); + } + } else if (originalUpperBound.length == 1) { + Type upperBound = resolveType(originalUpperBound[0], contextClass); + if (upperBound != originalUpperBound[0]) { + return ResolvableType.forWildCardTypeWithUpperBound( + wildcardType, ResolvableType.forType(upperBound)) + .getType(); + } + } + return wildcardType; + } } return genericType; } + private static ResolvableType resolveVariableConsiderBound(TypeVariable typeVariable, ResolvableType contextType) { + ResolvableType resolvedTypeArgument = resolveVariable(typeVariable, contextType); + if (resolvedTypeArgument == ResolvableType.NONE) { + resolvedTypeArgument = ResolvableType.forVariableBounds(typeVariable); + } + return resolvedTypeArgument; + } + private static ResolvableType resolveVariable(TypeVariable typeVariable, ResolvableType contextType) { ResolvableType resolvedType; if (contextType.hasGenerics()) { 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 af01421dc27..8b51ecb0b2c 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -98,6 +98,8 @@ public class ResolvableType implements Serializable { private static final ConcurrentReferenceHashMap cache = new ConcurrentReferenceHashMap<>(256); + private static final Type[] EMPTY_TYPE_ARRAY = new Type[0]; + /** * The underlying Java type being managed. @@ -616,7 +618,8 @@ public class ResolvableType implements Serializable { ResolvableType[] generics = getGenerics(); for (ResolvableType generic : generics) { - if (generic.isUnresolvableTypeVariable() || generic.isWildcardWithoutBounds() || + if (generic.isUnresolvableTypeVariable() || + generic.isUnresolvableWildcard(currentTypeSeen(alreadySeen)) || generic.hasUnresolvableGenerics(currentTypeSeen(alreadySeen))) { return true; } @@ -676,14 +679,32 @@ public class ResolvableType implements Serializable { if (this.type instanceof WildcardType wildcardType) { if (wildcardType.getLowerBounds().length == 0) { Type[] upperBounds = wildcardType.getUpperBounds(); - if (upperBounds.length == 0 || (upperBounds.length == 1 && Object.class == upperBounds[0])) { - return true; - } + return upperBounds.length == 0 || (upperBounds.length == 1 && (Object.class == upperBounds[0])); } } return false; } + /** + * Determine whether the underlying type represents a wildcard + * has unresolvable upper bound or lower bound, or simply without bound + */ + private boolean isUnresolvableWildcard(Set alreadySeen) { + if (this.type instanceof WildcardType wildcardType) { + Type[] lowerBounds = wildcardType.getLowerBounds(); + if (lowerBounds.length == 1) { + ResolvableType lowerResolvable = ResolvableType.forType(lowerBounds[0], this.variableResolver); + return lowerResolvable.isUnresolvableTypeVariable() || lowerResolvable.determineUnresolvableGenerics(alreadySeen); + } + Type[] upperBounds = wildcardType.getUpperBounds(); + if (upperBounds.length == 1 && upperBounds[0] != Object.class) { + ResolvableType upperResolvable = ResolvableType.forType(upperBounds[0], this.variableResolver); + return upperResolvable.isUnresolvableTypeVariable() || upperResolvable.determineUnresolvableGenerics(alreadySeen); + } + } + return isWildcardWithoutBounds(); + } + /** * Return a {@code ResolvableType} for the specified nesting level. *

See {@link #getNested(int, Map)} for details. @@ -1185,6 +1206,51 @@ public class ResolvableType implements Serializable { (generics != null ? new TypeVariablesVariableResolver(variables, generics) : null)); } + /** + * Return a {@code ResolvableType} for the specified {@link WildcardType} with pre-declared upper bound. + * @param wildcardType the WildcardType to introspect + * @param upperBound the upper bound of the wildcardType + * @return a {@code ResolvableType} for the specific wildcardType and upperBound + */ + public static ResolvableType forWildCardTypeWithUpperBound(WildcardType wildcardType, ResolvableType upperBound) { + Assert.notNull(wildcardType, "WildcardType must not be null"); + Assert.notNull(upperBound, "UpperBound must not be null"); + Type[] originalLowerBound = wildcardType.getLowerBounds(); + Assert.isTrue(originalLowerBound.length == 0, + () -> "The WildcardType has lower bound while upper bound provided " + wildcardType); + + Type upperBoundType = upperBound.getType(); + VariableResolver variableResolver = upperBoundType instanceof TypeVariable typeVariable + ? new TypeVariablesVariableResolver( + new TypeVariable[]{typeVariable}, new ResolvableType[]{upperBound}) + : null; + + return forType(new WildcardTypeImpl(new Type[]{upperBoundType}, EMPTY_TYPE_ARRAY), variableResolver); + } + + /** + * Return a {@code ResolvableType} for the specified {@link WildcardType} with pre-declared lower bound. + * @param wildcardType the WildcardType to introspect + * @param lowerBound the lower bound of the wildcardType + * @return a {@code ResolvableType} for the specific wildcardType and lowerBound + */ + public static ResolvableType forWildCardTypeWithLowerBound(WildcardType wildcardType, ResolvableType lowerBound) { + Assert.notNull(wildcardType, "WildcardType must not be null"); + Assert.notNull(lowerBound, "LowerBound must not be null"); + Type[] originalUpperBound = wildcardType.getUpperBounds(); + Assert.isTrue(originalUpperBound.length == 0 || originalUpperBound[0] == Object.class, + () -> "The WildcardType has upper bound %s while lower bound provided %s" + .formatted(originalUpperBound[0], wildcardType)); + + Type lowerBoundType = lowerBound.getType(); + VariableResolver variableResolver = lowerBoundType instanceof TypeVariable typeVariable + ? new TypeVariablesVariableResolver( + new TypeVariable[]{typeVariable}, new ResolvableType[]{lowerBound}) + : null; + + return forType(new WildcardTypeImpl(new Type[]{Object.class}, new Type[]{lowerBoundType}), variableResolver); + } + /** * Return a {@code ResolvableType} for the specified instance. The instance does not * convey generic information but if it implements {@link ResolvableTypeProvider} a @@ -1628,6 +1694,56 @@ public class ResolvableType implements Serializable { } + private static final class WildcardTypeImpl implements WildcardType, Serializable { + + private final Type[] upperBound; + private final Type[] lowerBound; + + private WildcardTypeImpl(Type[] upperBound, Type[] lowerBound) { + this.upperBound = upperBound; + this.lowerBound = lowerBound; + } + + @Override + public Type[] getUpperBounds() { + return upperBound.clone(); + } + + @Override + public Type[] getLowerBounds() { + return lowerBound.clone(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof WildcardType that)) { + return false; + } + return Arrays.equals(upperBound, that.getUpperBounds()) && Arrays.equals(lowerBound, that.getLowerBounds()); + } + + @Override + public int hashCode() { + return Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()); + } + + @Override + public String toString() { + if (getLowerBounds().length == 1) { + return "? super " + typeToString(getLowerBounds()[0]); + } + if (getUpperBounds().length == 0 || getUpperBounds()[0] == Object.class) { + return "?"; + } + return "? extends " + typeToString(getUpperBounds()[0]); + } + + private static String typeToString(Type type) { + return type instanceof Class cls ? cls.getName() : type.toString(); + } + } + + private static final class SyntheticParameterizedType implements ParameterizedType, Serializable { private final Type rawType; diff --git a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java index 265e1e800d6..98e7b00cd08 100644 --- a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java @@ -22,12 +22,16 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Supplier; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.core.GenericTypeResolver.getTypeVariableMap; @@ -250,6 +254,14 @@ class GenericTypeResolverTests { assertThat(resolvedType).isEqualTo(InheritsDefaultMethod.ConcreteType.class); } + @ParameterizedTest + @ValueSource(strings = {"getUpperBound", "getLowerBound"}) + void resolveTypeFromWildcardType(String methodName) { + Type type = method(MyInterfaceType.class, methodName).getGenericReturnType(); + Type resolvedType = resolveType(type, MySimpleInterfaceType.class); + assertThat(resolvedType).isEqualTo(method(MySimpleInterfaceType.class, methodName).getGenericReturnType()); + } + private static Method method(Class target, String methodName, Class... parameterTypes) { Method method = findMethod(target, methodName, parameterTypes); assertThat(method).describedAs(target.getName() + "#" + methodName).isNotNull(); @@ -258,9 +270,25 @@ class GenericTypeResolverTests { public interface MyInterfaceType { + default Optional getUpperBound() { + return Optional.empty(); + } + + default List getLowerBound() { + return Collections.emptyList(); + } } public class MySimpleInterfaceType implements MyInterfaceType { + @Override + public Optional getUpperBound() { + return MyInterfaceType.super.getUpperBound(); + } + + @Override + public List getLowerBound() { + return MyInterfaceType.super.getLowerBound(); + } } public class MyCollectionInterfaceType implements MyInterfaceType> { 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 c01ade2347b..67ff0efa8d6 100644 --- a/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java +++ b/spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java @@ -41,10 +41,13 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.Callable; +import java.util.stream.Stream; import org.assertj.core.api.AbstractAssert; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.junit.jupiter.MockitoExtension; @@ -1549,6 +1552,31 @@ class ResolvableTypeTests { assertThat(typeWithGenerics.isAssignableFrom(PaymentCreator.class)).isTrue(); } + @ParameterizedTest + @MethodSource("wildcardInfo") + void gh36474(ResolvableType typeVariable, Class resolved) { + assertThat(typeVariable.resolve()).isEqualTo(resolved); + } + + + static Stream wildcardInfo() throws Exception { + WildcardType listxs = getWildcardType(AssignmentBase.class, "listxs"); + WildcardType listsc = getWildcardType(AssignmentBase.class, "listsc"); + ResolvableType owner = ResolvableType.forType(Assignment.class).as(AssignmentBase.class); + + ResolvableType lbWildcard = ResolvableType.forWildCardTypeWithUpperBound( + listxs, ResolvableType.forType(listxs.getUpperBounds()[0], owner)); + ResolvableType ubWildcard = ResolvableType.forWildCardTypeWithLowerBound( + listsc, ResolvableType.forType(listsc.getLowerBounds()[0], owner)); + return Stream.of(new Object[] {lbWildcard, String.class}, new Object[] {ubWildcard, CharSequence.class}); + } + + + static WildcardType getWildcardType(Class cls, String field) throws Exception { + ResolvableType type = ResolvableType.forField(cls.getField(field)); + return (WildcardType) type.getGeneric(0).getType(); + } + private ResolvableType testSerialization(ResolvableType type) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -1579,13 +1607,13 @@ class ResolvableTypeTests { @SuppressWarnings("unused") private HashMap> myMap; - @SuppressWarnings("serial") static class ExtendsList extends ArrayList { - } + } @SuppressWarnings("serial") static class ExtendsMap extends HashMap { + }