Browse Source

Fix generic with WildcardType return type support in HttpServiceMethod

Signed-off-by: anaconda875 <hflbtmax@gmail.com>
pull/36475/head
anaconda875 2 days ago
parent
commit
018487e2ba
  1. 42
      spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java
  2. 124
      spring-core/src/main/java/org/springframework/core/ResolvableType.java
  3. 28
      spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java
  4. 32
      spring-core/src/test/java/org/springframework/core/ResolvableTypeTests.java

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

@ -154,11 +154,8 @@ public final class GenericTypeResolver { @@ -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 { @@ -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 { @@ -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 { @@ -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()) {

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

@ -98,6 +98,8 @@ public class ResolvableType implements Serializable { @@ -98,6 +98,8 @@ public class ResolvableType implements Serializable {
private static final ConcurrentReferenceHashMap<ResolvableType, ResolvableType> 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 { @@ -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 { @@ -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<Type> 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.
* <p>See {@link #getNested(int, Map)} for details.
@ -1185,6 +1206,51 @@ public class ResolvableType implements Serializable { @@ -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 { @@ -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;

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

@ -22,12 +22,16 @@ import java.lang.reflect.ParameterizedType; @@ -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 { @@ -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 { @@ -258,9 +270,25 @@ class GenericTypeResolverTests {
public interface MyInterfaceType<T> {
default Optional<? extends T> getUpperBound() {
return Optional.empty();
}
default List<? super T> getLowerBound() {
return Collections.emptyList();
}
}
public class MySimpleInterfaceType implements MyInterfaceType<String> {
@Override
public Optional<? extends String> getUpperBound() {
return MyInterfaceType.super.getUpperBound();
}
@Override
public List<? super String> getLowerBound() {
return MyInterfaceType.super.getLowerBound();
}
}
public class MyCollectionInterfaceType implements MyInterfaceType<Collection<String>> {

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

@ -41,10 +41,13 @@ import java.util.Set; @@ -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 { @@ -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<Object[]> 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 { @@ -1579,13 +1607,13 @@ class ResolvableTypeTests {
@SuppressWarnings("unused")
private HashMap<Integer, List<String>> myMap;
@SuppressWarnings("serial")
static class ExtendsList extends ArrayList<CharSequence> {
}
}
@SuppressWarnings("serial")
static class ExtendsMap extends HashMap<String, Integer> {
}

Loading…
Cancel
Save