Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -179,18 +176,16 @@ 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;
}
else {
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 {
Expand All @@ -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()) {
Expand Down
127 changes: 123 additions & 4 deletions spring-core/src/main/java/org/springframework/core/ResolvableType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -616,7 +618,9 @@ private boolean determineUnresolvableGenerics(@Nullable Set<Type> 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;
}
Expand Down Expand Up @@ -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<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 false;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@
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;
Expand Down Expand Up @@ -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());
}

Expand All @@ -268,12 +280,29 @@ private static Method method(Class<?> target, String methodName, Class<?>... par


public interface MyInterfaceType<T> {
default Optional<? extends T> getUpperBound() {
return Optional.empty();
}

default List<? super T> getLowerBound() {
return Collections.emptyList();
}

default T get() {
return null;
}
}

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 MyParameterizedInterfaceType<P> implements MyInterfaceType<Collection<P>> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<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();
Expand Down
Loading