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 7bb5961727d..eb00d48d0f6 100644 --- a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java +++ b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java @@ -21,8 +21,12 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; /** * Helper class for resolving generic types against type variables. @@ -38,6 +42,11 @@ import org.springframework.util.Assert; */ public abstract class GenericTypeResolver { + /** Cache from Class to TypeVariable Map */ + @SuppressWarnings("rawtypes") + private static final Map, Map> typeVariableCache = new ConcurrentReferenceHashMap<>(); + + /** * Determine the target type for the given generic parameter type. * @param methodParameter the method parameter specification @@ -196,4 +205,80 @@ public abstract class GenericTypeResolver { return ResolvableType.NONE; } + /** + * Resolve the specified generic type against the given TypeVariable map. + *

Used by Spring Data. + * @param genericType the generic type to resolve + * @param map the TypeVariable Map to resolved against + * @return the type if it resolves to a Class, or {@code Object.class} otherwise + */ + @SuppressWarnings("rawtypes") + public static Class resolveType(Type genericType, Map map) { + return ResolvableType.forType(genericType, new TypeVariableMapVariableResolver(map)).resolve(Object.class); + } + + /** + * Build a mapping of {@link TypeVariable#getName TypeVariable names} to + * {@link Class concrete classes} for the specified {@link Class}. + * Searches all super types, enclosing types and interfaces. + * @see #resolveType(Type, Map) + */ + @SuppressWarnings("rawtypes") + public static Map getTypeVariableMap(Class clazz) { + Map typeVariableMap = typeVariableCache.get(clazz); + if (typeVariableMap == null) { + typeVariableMap = new HashMap<>(); + buildTypeVariableMap(ResolvableType.forClass(clazz), typeVariableMap); + typeVariableCache.put(clazz, Collections.unmodifiableMap(typeVariableMap)); + } + return typeVariableMap; + } + + @SuppressWarnings("rawtypes") + private static void buildTypeVariableMap(ResolvableType type, Map typeVariableMap) { + if (type != ResolvableType.NONE) { + if (type.getType() instanceof ParameterizedType) { + TypeVariable[] variables = type.resolve().getTypeParameters(); + for (int i = 0; i < variables.length; i++) { + ResolvableType generic = type.getGeneric(i); + while (generic.getType() instanceof TypeVariable) { + generic = generic.resolveType(); + } + if (generic != ResolvableType.NONE) { + typeVariableMap.put(variables[i], generic.getType()); + } + } + } + buildTypeVariableMap(type.getSuperType(), typeVariableMap); + for (ResolvableType interfaceType : type.getInterfaces()) { + buildTypeVariableMap(interfaceType, typeVariableMap); + } + if (type.resolve().isMemberClass()) { + buildTypeVariableMap(ResolvableType.forClass(type.resolve().getEnclosingClass()), typeVariableMap); + } + } + } + + + @SuppressWarnings({"serial", "rawtypes"}) + private static class TypeVariableMapVariableResolver implements ResolvableType.VariableResolver { + + private final Map typeVariableMap; + + public TypeVariableMapVariableResolver(Map typeVariableMap) { + this.typeVariableMap = typeVariableMap; + } + + @Override + public ResolvableType resolveVariable(TypeVariable variable) { + Type type = this.typeVariableMap.get(variable); + return (type != null ? ResolvableType.forType(type) : null); + } + + @Override + public Object getSource() { + return this.typeVariableMap; + } + } + } 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 7d18f6b9a42..9244e502c77 100644 --- a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java @@ -17,7 +17,11 @@ package org.springframework.core; import java.io.Serializable; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -32,7 +36,7 @@ import static org.springframework.util.ReflectionUtils.*; * @author Juergen Hoeller * @author Sam Brannen */ -@SuppressWarnings({ "unchecked", "rawtypes" }) +@SuppressWarnings({"unchecked", "rawtypes"}) public class GenericTypeResolverTests { @Test @@ -72,11 +76,69 @@ public class GenericTypeResolverTests { resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "object"), MyInterfaceType.class)); } + @Test + public void testResolveType() { + Method intMessageMethod = findMethod(MyTypeWithMethods.class, "readIntegerInputMessage", MyInterfaceType.class); + MethodParameter intMessageMethodParam = new MethodParameter(intMessageMethod, 0); + assertEquals(MyInterfaceType.class, + resolveType(intMessageMethodParam.getGenericParameterType(), new HashMap<>())); + + Method intArrMessageMethod = findMethod(MyTypeWithMethods.class, "readIntegerArrayInputMessage", + MyInterfaceType[].class); + MethodParameter intArrMessageMethodParam = new MethodParameter(intArrMessageMethod, 0); + assertEquals(MyInterfaceType[].class, + resolveType(intArrMessageMethodParam.getGenericParameterType(), new HashMap<>())); + + Method genericArrMessageMethod = findMethod(MySimpleTypeWithMethods.class, "readGenericArrayInputMessage", + Object[].class); + MethodParameter genericArrMessageMethodParam = new MethodParameter(genericArrMessageMethod, 0); + Map varMap = getTypeVariableMap(MySimpleTypeWithMethods.class); + assertEquals(Integer[].class, resolveType(genericArrMessageMethodParam.getGenericParameterType(), varMap)); + } + @Test public void testBoundParameterizedType() { assertEquals(B.class, resolveTypeArgument(TestImpl.class, TestIfc.class)); } + @Test + public void testGetTypeVariableMap() throws Exception { + Map map; + + map = GenericTypeResolver.getTypeVariableMap(MySimpleInterfaceType.class); + assertThat(map.toString(), equalTo("{T=class java.lang.String}")); + + map = GenericTypeResolver.getTypeVariableMap(MyCollectionInterfaceType.class); + assertThat(map.toString(), equalTo("{T=java.util.Collection}")); + + map = GenericTypeResolver.getTypeVariableMap(MyCollectionSuperclassType.class); + assertThat(map.toString(), equalTo("{T=java.util.Collection}")); + + map = GenericTypeResolver.getTypeVariableMap(MySimpleTypeWithMethods.class); + assertThat(map.toString(), equalTo("{T=class java.lang.Integer}")); + + map = GenericTypeResolver.getTypeVariableMap(TopLevelClass.class); + assertThat(map.toString(), equalTo("{}")); + + map = GenericTypeResolver.getTypeVariableMap(TypedTopLevelClass.class); + assertThat(map.toString(), equalTo("{T=class java.lang.Integer}")); + + map = GenericTypeResolver.getTypeVariableMap(TypedTopLevelClass.TypedNested.class); + assertThat(map.size(), equalTo(2)); + Type t = null; + Type x = null; + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey().toString().equals("T")) { + t = entry.getValue(); + } + else { + x = entry.getValue(); + } + } + assertThat(t, equalTo((Type) Integer.class)); + assertThat(x, equalTo((Type) Long.class)); + } + @Test public void getGenericsCannotBeResolved() throws Exception { // SPR-11030