Merge pull request #97 from sbrannen/SPR-9493

* SPR-9493:
  Infer return type of parametrized factory methods
This commit is contained in:
Sam Brannen 2012-06-19 18:07:28 +02:00
commit 64d6605974
7 changed files with 341 additions and 41 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -63,6 +63,7 @@ import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
@ -106,6 +107,7 @@ import org.springframework.util.StringUtils;
* @author Mark Fisher
* @author Costin Leau
* @author Chris Beams
* @author Sam Brannen
* @since 13.02.2004
* @see RootBeanDefinition
* @see DefaultListableBeanFactory
@ -628,16 +630,26 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
return null;
}
List<ValueHolder> argumentValues = mbd.getConstructorArgumentValues().getGenericArgumentValues();
Object[] args = new Object[argumentValues.size()];
for (int i = 0; i < args.length; i++) {
args[i] = argumentValues.get(i).getValue();
}
// If all factory methods have the same return type, return that type.
// Can't clearly figure out exact method due to type converting / autowiring!
int minNrOfArgs = mbd.getConstructorArgumentValues().getArgumentCount();
Method[] candidates = ReflectionUtils.getUniqueDeclaredMethods(factoryClass);
Set<Class> returnTypes = new HashSet<Class>(1);
Set<Class<?>> returnTypes = new HashSet<Class<?>>(1);
for (Method factoryMethod : candidates) {
if (Modifier.isStatic(factoryMethod.getModifiers()) == isStatic &&
factoryMethod.getName().equals(mbd.getFactoryMethodName()) &&
factoryMethod.getParameterTypes().length >= minNrOfArgs) {
returnTypes.add(factoryMethod.getReturnType());
if (Modifier.isStatic(factoryMethod.getModifiers()) == isStatic
&& factoryMethod.getName().equals(mbd.getFactoryMethodName())
&& factoryMethod.getParameterTypes().length >= minNrOfArgs) {
Class<?> returnType = GenericTypeResolver.resolveParameterizedReturnType(factoryMethod, args);
if (returnType != null) {
returnTypes.add(returnType);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2009 the original author or authors.
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -29,6 +29,8 @@ import java.util.Map;
import java.util.Set;
import static org.junit.Assert.*;
import org.easymock.EasyMock;
import org.junit.Test;
import test.beans.GenericBean;
import test.beans.GenericIntegerBean;
@ -46,6 +48,7 @@ import org.springframework.core.io.UrlResource;
/**
* @author Juergen Hoeller
* @author Chris Beams
* @author Sam Brannen
* @since 20.01.2006
*/
public class BeanFactoryGenericsTests {
@ -619,6 +622,30 @@ public class BeanFactoryGenericsTests {
assertEquals(new URL("http://www.springframework.org"), us.iterator().next());
}
/**
* Tests support for parameterized {@code factory-method} declarations such
* as EasyMock's {@code createMock()} method which has the following signature.
*
* <pre>{@code
* public static <T> T createMock(Class<T> toMock)
* }</pre>
*
* @since 3.2
* @see SPR-9493
*/
@Test
public void parameterizedFactoryMethod() {
RootBeanDefinition rbd = new RootBeanDefinition(EasyMock.class);
rbd.setFactoryMethodName("createMock");
rbd.getConstructorArgumentValues().addGenericArgumentValue(Runnable.class);
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.registerBeanDefinition("easyMock", rbd);
Map<String, Runnable> beans = bf.getBeansOfType(Runnable.class);
assertEquals(1, beans.size());
}
@SuppressWarnings("serial")
public static class NamedUrlList extends LinkedList<URL> {

View File

@ -28,7 +28,6 @@ import test.beans.TestBean;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.io.ClassPathResource;
/**

View File

@ -19,6 +19,10 @@
<level value="debug" />
</logger>
<logger name="org.springframework.core.GenericTypeResolver">
<level value="warn" />
</logger>
<!-- Root Logger -->
<root>
<priority value="warn" />

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -30,7 +30,11 @@ import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Helper class for resolving generic types against type variables.
@ -40,11 +44,14 @@ import org.springframework.util.Assert;
*
* @author Juergen Hoeller
* @author Rob Harrop
* @author Sam Brannen
* @since 2.5.2
* @see GenericCollectionTypeResolver
*/
public abstract class GenericTypeResolver {
private static final Log logger = LogFactory.getLog(GenericTypeResolver.class);
/** Cache from Class to TypeVariable Map */
private static final Map<Class, Reference<Map<TypeVariable, Type>>> typeVariableCache =
Collections.synchronizedMap(new WeakHashMap<Class, Reference<Map<TypeVariable, Type>>>());
@ -88,18 +95,144 @@ public abstract class GenericTypeResolver {
}
/**
* Determine the target type for the generic return type of the given method.
* Determine the target type for the generic return type of the given method,
* where the type variable is declared on the given class.
*
* @param method the method to introspect
* @param clazz the class to resolve type variables against
* @return the corresponding generic parameter or return type
* @see #resolveParameterizedReturnType
*/
public static Class<?> resolveReturnType(Method method, Class clazz) {
public static Class<?> resolveReturnType(Method method, Class<?> clazz) {
Assert.notNull(method, "Method must not be null");
Type genericType = method.getGenericReturnType();
Assert.notNull(clazz, "Class must not be null");
Map<TypeVariable, Type> typeVariableMap = getTypeVariableMap(clazz);
Type rawType = getRawType(genericType, typeVariableMap);
return (rawType instanceof Class ? (Class) rawType : method.getReturnType());
return (rawType instanceof Class ? (Class<?>) rawType : method.getReturnType());
}
/**
* Determine the target type for the generic return type of the given
* <em>parameterized</em> method, where the type variable is declared
* on the given method.
*
* <p>For example, given a factory method with the following signature,
* if {@code resolveParameterizedReturnType()} is invoked with the reflected
* method for {@code creatProxy()} and an {@code Object[]} array containing
* {@code MyService.class}, {@code resolveParameterizedReturnType()} will
* infer that the target return type is {@code MyService}.
*
* <pre>{@code public static <T> T createProxy(Class<T> clazz)}</pre>
*
* <h4>Possible Return Values</h4>
* <ul>
* <li>the target return type if it can be inferred</li>
* <li>the {@link Method#getReturnType() standard return type}, if
* the given {@code method} does not declare any {@link
* Method#getTypeParameters() generic types}</li>
* <li>the {@link Method#getReturnType() standard return type}, if the
* target return type cannot be inferred (e.g., due to type erasure)</li>
* <li>{@code null}, if the length of the given arguments array is shorter
* than the length of the {@link
* Method#getGenericParameterTypes() formal argument list} for the given
* method</li>
* </ul>
*
* @param method the method to introspect, never {@code null}
* @param args the arguments that will be supplied to the method when it is
* invoked, never {@code null}
* @return the resolved target return type, the standard return type, or
* {@code null}
* @since 3.2
* @see #resolveReturnType
*/
public static Class<?> resolveParameterizedReturnType(Method method, Object[] args) {
Assert.notNull(method, "method must not be null");
Assert.notNull(args, "args must not be null");
final TypeVariable<Method>[] declaredGenericTypes = method.getTypeParameters();
final Type genericReturnType = method.getGenericReturnType();
final Type[] genericArgumentTypes = method.getGenericParameterTypes();
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"Resolving parameterized return type for [%s] with concrete method arguments [%s].",
method.toGenericString(), ObjectUtils.nullSafeToString(args)));
}
// No declared generic types to inspect, so just return the standard return type.
if (declaredGenericTypes.length == 0) {
return method.getReturnType();
}
// The supplied argument list is too short for the method's signature, so
// return null, since such a method invocation would fail.
if (args.length < genericArgumentTypes.length) {
return null;
}
// Ensure that the generic type is declared directly on the method
// itself, not on the enclosing class or interface.
boolean locallyDeclaredGenericTypeMatchesReturnType = false;
for (TypeVariable<Method> currentType : declaredGenericTypes) {
if (currentType.equals(genericReturnType)) {
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"Found declared generic type [%s] that matches the target return type [%s].",
currentType, genericReturnType));
}
locallyDeclaredGenericTypeMatchesReturnType = true;
break;
}
}
if (locallyDeclaredGenericTypeMatchesReturnType) {
for (int i = 0; i < genericArgumentTypes.length; i++) {
final Type currentArgumentType = genericArgumentTypes[i];
if (currentArgumentType.equals(genericReturnType)) {
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"Found generic method argument at index [%s] that matches the target return type.", i));
}
return args[i].getClass();
}
if (currentArgumentType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) currentArgumentType;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (int j = 0; j < actualTypeArguments.length; j++) {
final Type typeArg = actualTypeArguments[j];
if (typeArg.equals(genericReturnType)) {
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"Found method argument at index [%s] that is parameterized with a type that matches the target return type.",
i));
}
if (args[i] instanceof Class) {
return (Class<?>) args[i];
} else {
// Consider adding logic to determine the class of the
// J'th typeArg, if possible.
logger.info(String.format(
"Could not determine the target type for parameterized type [%s] for method [%s].",
typeArg, method.toGenericString()));
// For now, just fall back...
return method.getReturnType();
}
}
}
}
}
}
// Fall back...
return method.getReturnType();
}
/**
@ -128,7 +261,7 @@ public abstract class GenericTypeResolver {
return null;
}
}
return GenericTypeResolver.resolveTypeArgument((Class<?>) returnType, genericIfc);
return resolveTypeArgument((Class<?>) returnType, genericIfc);
}
/**
@ -186,7 +319,7 @@ public abstract class GenericTypeResolver {
}
return null;
}
private static Class[] doResolveTypeArguments(Class ownerClass, Type ifc, Class genericIfc) {
if (ifc instanceof ParameterizedType) {
ParameterizedType paramIfc = (ParameterizedType) ifc;
@ -236,7 +369,6 @@ public abstract class GenericTypeResolver {
return (arg instanceof Class ? (Class) arg : Object.class);
}
/**
* Resolve the specified generic type against the given TypeVariable map.
* @param genericType the generic type to resolve
@ -272,9 +404,9 @@ public abstract class GenericTypeResolver {
}
/**
* Build a mapping of {@link TypeVariable#getName TypeVariable names} to concrete
* {@link Class} for the specified {@link Class}. Searches all super types,
* enclosing types and interfaces.
* 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.
*/
public static Map<TypeVariable, Type> getTypeVariableMap(Class clazz) {
Reference<Map<TypeVariable, Type>> ref = typeVariableCache.get(clazz);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,51 +16,116 @@
package org.springframework.core;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.junit.Test;
import org.springframework.util.ReflectionUtils;
import static org.junit.Assert.*;
import static org.springframework.core.GenericTypeResolver.*;
import static org.springframework.util.ReflectionUtils.*;
/**
* @author Juergen Hoeller
* @author Sam Brannen
*/
public class GenericTypeResolverTests {
@Test
public void testSimpleInterfaceType() {
assertEquals(String.class, GenericTypeResolver.resolveTypeArgument(MySimpleInterfaceType.class, MyInterfaceType.class));
public void simpleInterfaceType() {
assertEquals(String.class, resolveTypeArgument(MySimpleInterfaceType.class, MyInterfaceType.class));
}
@Test
public void testSimpleCollectionInterfaceType() {
assertEquals(Collection.class, GenericTypeResolver.resolveTypeArgument(MyCollectionInterfaceType.class, MyInterfaceType.class));
public void simpleCollectionInterfaceType() {
assertEquals(Collection.class, resolveTypeArgument(MyCollectionInterfaceType.class, MyInterfaceType.class));
}
@Test
public void testSimpleSuperclassType() {
assertEquals(String.class, GenericTypeResolver.resolveTypeArgument(MySimpleSuperclassType.class, MySuperclassType.class));
public void simpleSuperclassType() {
assertEquals(String.class, resolveTypeArgument(MySimpleSuperclassType.class, MySuperclassType.class));
}
@Test
public void testSimpleCollectionSuperclassType() {
assertEquals(Collection.class, GenericTypeResolver.resolveTypeArgument(MyCollectionSuperclassType.class, MySuperclassType.class));
public void simpleCollectionSuperclassType() {
assertEquals(Collection.class, resolveTypeArgument(MyCollectionSuperclassType.class, MySuperclassType.class));
}
@Test
public void testMethodReturnType() {
assertEquals(Integer.class, GenericTypeResolver.resolveReturnTypeArgument(ReflectionUtils.findMethod(MyTypeWithMethods.class, "integer"), MyInterfaceType.class));
assertEquals(String.class, GenericTypeResolver.resolveReturnTypeArgument(ReflectionUtils.findMethod(MyTypeWithMethods.class, "string"), MyInterfaceType.class));
assertEquals(null, GenericTypeResolver.resolveReturnTypeArgument(ReflectionUtils.findMethod(MyTypeWithMethods.class, "raw"), MyInterfaceType.class));
assertEquals(null, GenericTypeResolver.resolveReturnTypeArgument(ReflectionUtils.findMethod(MyTypeWithMethods.class, "object"), MyInterfaceType.class));
}
@Test
public void testNullIfNotResolvable() {
public void nullIfNotResolvable() {
GenericClass<String> obj = new GenericClass<String>();
assertNull(GenericTypeResolver.resolveTypeArgument(obj.getClass(), GenericClass.class));
assertNull(resolveTypeArgument(obj.getClass(), GenericClass.class));
}
@Test
public void methodReturnTypes() {
assertEquals(Integer.class, resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "integer"), MyInterfaceType.class));
assertEquals(String.class, resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "string"), MyInterfaceType.class));
assertEquals(null, resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "raw"), MyInterfaceType.class));
assertEquals(null, resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "object"), MyInterfaceType.class));
}
/**
* @since 3.2
*/
@Test
public void parameterizedMethodReturnTypes() {
Method notParameterized = findMethod(MyTypeWithMethods.class, "notParameterized", new Class[] {});
assertEquals(String.class, resolveParameterizedReturnType(notParameterized, new Object[] {}));
Method notParameterizedWithArguments = findMethod(MyTypeWithMethods.class, "notParameterizedWithArguments",
new Class[] { Integer.class, Boolean.class });
assertEquals(String.class,
resolveParameterizedReturnType(notParameterizedWithArguments, new Object[] { 99, true }));
Method createProxy = findMethod(MyTypeWithMethods.class, "createProxy", new Class[] { Object.class });
assertEquals(String.class, resolveParameterizedReturnType(createProxy, new Object[] { "foo" }));
Method createNamedProxyWithDifferentTypes = findMethod(MyTypeWithMethods.class, "createNamedProxy",
new Class[] { String.class, Object.class });
// one argument to few
assertNull(resolveParameterizedReturnType(createNamedProxyWithDifferentTypes, new Object[] { "enigma" }));
assertEquals(Long.class,
resolveParameterizedReturnType(createNamedProxyWithDifferentTypes, new Object[] { "enigma", 99L }));
Method createNamedProxyWithDuplicateTypes = findMethod(MyTypeWithMethods.class, "createNamedProxy",
new Class[] { String.class, Object.class });
assertEquals(String.class,
resolveParameterizedReturnType(createNamedProxyWithDuplicateTypes, new Object[] { "enigma", "foo" }));
Method createMock = findMethod(MyTypeWithMethods.class, "createMock", new Class[] { Class.class });
assertEquals(Runnable.class, resolveParameterizedReturnType(createMock, new Object[] { Runnable.class }));
Method createNamedMock = findMethod(MyTypeWithMethods.class, "createNamedMock", new Class[] { String.class,
Class.class });
assertEquals(Runnable.class,
resolveParameterizedReturnType(createNamedMock, new Object[] { "foo", Runnable.class }));
Method createVMock = findMethod(MyTypeWithMethods.class, "createVMock",
new Class[] { Object.class, Class.class });
assertEquals(Runnable.class,
resolveParameterizedReturnType(createVMock, new Object[] { "foo", Runnable.class }));
// Ideally we would expect String.class instead of Object.class, but
// resolveParameterizedReturnType() does not currently support this form of
// look-up.
Method extractValueFrom = findMethod(MyTypeWithMethods.class, "extractValueFrom",
new Class[] { MyInterfaceType.class });
assertEquals(Object.class,
resolveParameterizedReturnType(extractValueFrom, new Object[] { new MySimpleInterfaceType() }));
// Ideally we would expect Boolean.class instead of Object.class, but this
// information is not available at run-time due to type erasure.
Map<Integer, Boolean> map = new HashMap<Integer, Boolean>();
map.put(0, false);
map.put(1, true);
Method extractMagicValue = findMethod(MyTypeWithMethods.class, "extractMagicValue", new Class[] { Map.class });
assertEquals(Object.class, resolveParameterizedReturnType(extractMagicValue, new Object[] { map }));
}
@ -73,7 +138,6 @@ public class GenericTypeResolverTests {
public class MyCollectionInterfaceType implements MyInterfaceType<Collection<String>> {
}
public abstract class MySuperclassType<T> {
}
@ -83,12 +147,70 @@ public class GenericTypeResolverTests {
public class MyCollectionSuperclassType extends MySuperclassType<Collection<String>> {
}
public class MyTypeWithMethods {
public static class MyTypeWithMethods {
public MyInterfaceType<Integer> integer() { return null; }
public MySimpleInterfaceType string() { return null; }
public Object object() { return null; }
@SuppressWarnings("rawtypes")
public MyInterfaceType raw() { return null; }
public String notParameterized() { return null; }
public String notParameterizedWithArguments(Integer x, Boolean b) { return null; }
/**
* Simulates a factory method that wraps the supplied object in a proxy
* of the same type.
*/
public static <T> T createProxy(T object) {
return null;
}
/**
* Similar to {@link #createProxy(Object)} but adds an additional argument
* before the argument of type {@code T}. Note that they may potentially
* be of the same time when invoked!
*/
public static <T> T createNamedProxy(String name, T object) {
return null;
}
/**
* Simulates factory methods found in libraries such as Mockito and EasyMock.
*/
public static <MOCK> MOCK createMock(Class<MOCK> toMock) {
return null;
}
/**
* Similar to {@link #createMock(Class)} but adds an additional method
* argument before the parameterized argument.
*/
public static <T> T createNamedMock(String name, Class<T> toMock) {
return null;
}
/**
* Similar to {@link #createNamedMock(String, Class)} but adds an additional
* parameterized type.
*/
public static <V extends Object, T> T createVMock(V name, Class<T> toMock) {
return null;
}
/**
* Extract some value of the type supported by the interface (i.e., by
* a concrete, non-generic implementation of the interface).
*/
public static <T> T extractValueFrom(MyInterfaceType<T> myInterfaceType) {
return null;
}
/**
* Extract some magic value from the supplied map.
*/
public static <K, V> V extractMagicValue(Map<K, V> map) {
return null;
}
}
static class GenericClass<T> {

View File

@ -15,6 +15,10 @@
<level value="warn" />
</logger>
<logger name="org.springframework.core.GenericTypeResolver">
<level value="warn" />
</logger>
<!-- Root Logger -->
<root>
<priority value="warn" />