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 08396473273..2e82f413ead 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 static Class resolveReturnType(Method method, Class clazz) { 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) { Type type = resolvedTypeVariable.getType(); if (type instanceof ParameterizedType) { @@ -179,10 +176,8 @@ else if (genericType instanceof ParameterizedType parameterizedType) { 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; } @@ -190,7 +185,7 @@ else if (genericType instanceof ParameterizedType parameterizedType) { 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 { @@ -203,10 +198,39 @@ else if (typeArgument instanceof ParameterizedType) { } } } + 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 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..0c7c14492b9 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,9 @@ private boolean determineUnresolvableGenerics(@Nullable Set alreadySeen) { ResolvableType[] generics = getGenerics(); for (ResolvableType generic : generics) { - if (generic.isUnresolvableTypeVariable() || generic.isWildcardWithoutBounds() || + if (generic.isUnresolvableTypeVariable() || + generic.isWildcardWithoutBounds() || + generic.isUnresolvableWildcard(currentTypeSeen(alreadySeen)) || generic.hasUnresolvableGenerics(currentTypeSeen(alreadySeen))) { return true; } @@ -676,9 +680,27 @@ private boolean isWildcardWithoutBounds() { 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. + */ + 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 false; @@ -1185,6 +1207,51 @@ public static ResolvableType forClassWithGenerics(Class clazz, @Nullable Reso (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 +1695,58 @@ public Object getSource() { } + 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 this.upperBound.clone(); + } + + @Override + public Type[] getLowerBounds() { + return this.lowerBound.clone(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof WildcardType that)) { + return false; + } + return Arrays.equals(this.upperBound, that.getUpperBounds()) && + Arrays.equals(this.lowerBound, that.getLowerBounds()); + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.lowerBound) ^ Arrays.hashCode(this.upperBound); + } + + @Override + public String toString() { + if (this.lowerBound.length == 1) { + return "? super " + typeToString(this.lowerBound[0]); + } + if (this.upperBound.length == 0 || this.upperBound[0] == Object.class) { + return "?"; + } + return "? extends " + typeToString(this.upperBound[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 7e4e3542c02..8378d15cc9b 100644 --- a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java @@ -22,6 +22,7 @@ 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; @@ -29,6 +30,8 @@ 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; @@ -251,12 +254,21 @@ void resolveTypeFromGenericDefaultMethod() { 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()); + } + @Test void resolveTypeFromNestedParameterizedType() { - Type resolvedType = resolveType(method(MyInterfaceType.class, "get").getGenericReturnType(), MyCollectionInterfaceType.class); + Type rawReturnType = method(MyInterfaceType.class, "get").getGenericReturnType(); + Type resolvedType = resolveType(rawReturnType, MyCollectionInterfaceType.class); assertThat(resolvedType).isEqualTo(method(MyCollectionInterfaceType.class, "get").getGenericReturnType()); - resolvedType = resolveType(method(MyInterfaceType.class, "get").getGenericReturnType(), MyOptionalInterfaceType.class); + resolvedType = resolveType(rawReturnType, MyOptionalInterfaceType.class); assertThat(resolvedType).isEqualTo(method(MyOptionalInterfaceType.class, "get").getGenericReturnType()); } @@ -268,12 +280,29 @@ private static Method method(Class target, String methodName, Class... par public interface MyInterfaceType { + default Optional getUpperBound() { + return Optional.empty(); + } + + default List getLowerBound() { + return Collections.emptyList(); + } + default T get() { return null; } } public class MySimpleInterfaceType implements MyInterfaceType { + @Override + public Optional getUpperBound() { + return MyInterfaceType.super.getUpperBound(); + } + + @Override + public List getLowerBound() { + return MyInterfaceType.super.getLowerBound(); + } } public class MyParameterizedInterfaceType

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 8b67d70d783..6d6d6cbe1ae 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.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 @@ void gh34541() throws Exception { 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(); diff --git a/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java b/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java index 95a8d6b8ec4..0ec36e5cb42 100644 --- a/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java @@ -25,6 +25,7 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.LinkedHashSet; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; @@ -103,6 +104,12 @@ void shutdown() { @interface ParameterizedAdapterTest { } + public static Stream wildcardCases() { + return Stream.of( + (Function>) BaseClient::getListWildcardUpperBound1, + (Function>) BaseClient::getListWildcardUpperBound2); + } + public static Stream arguments() throws IOException { return Stream.of( createArgsForAdapter((url, or) -> { @@ -216,6 +223,15 @@ void getEntityWithGenericReturnType() { assertThat(entity.getBody().name()).isEqualTo("Karl"); } + @ParameterizedTest + @MethodSource("wildcardCases") + void getWildcardReturnType(Function> invocation) { + PersonClient client = initService(PersonClient.class); + prepareResponse(r -> r.setHeader("Content-Type", "application/json").body("[{\"name\":\"Karl\"}]")); + List list = invocation.apply(client); + assertThat(list.get(0).name()).isEqualTo("Karl"); + } + @ParameterizedAdapterTest void getWithUriBuilderFactory(MockWebServer server, Service service) throws InterruptedException { prepareResponse(builder -> @@ -467,6 +483,12 @@ private interface BaseClient { @GetExchange T getBody(); + @GetExchange + List getListWildcardUpperBound1(); + + @GetExchange + List getListWildcardUpperBound2(); + @GetExchange ResponseEntity getEntity(); }