diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java index 67ec455140e..4a67ca68695 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java @@ -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 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 returnTypes = new HashSet(1); + Set> returnTypes = new HashSet>(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); + } } } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java index 1280b0207f5..be2cfdf2979 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanFactoryGenericsTests.java @@ -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. + * + *
{@code
+	 * public static  T createMock(Class toMock)
+	 * }
+ * + * @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 beans = bf.getBeansOfType(Runnable.class); + assertEquals(1, beans.size()); + } + @SuppressWarnings("serial") public static class NamedUrlList extends LinkedList { diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java index 5ecb0d7bd71..696ab6c0afe 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/FactoryMethodTests.java @@ -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; /** diff --git a/spring-beans/src/test/resources/log4j.xml b/spring-beans/src/test/resources/log4j.xml index 767b96d6206..37f573d2aef 100644 --- a/spring-beans/src/test/resources/log4j.xml +++ b/spring-beans/src/test/resources/log4j.xml @@ -19,6 +19,10 @@ + + + + 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 1111a1268ce..d4dda19c52b 100644 --- a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java +++ b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java @@ -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>> typeVariableCache = Collections.synchronizedMap(new WeakHashMap>>()); @@ -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 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 + * parameterized method, where the type variable is declared + * on the given method. + * + *

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}. + * + *

{@code public static  T createProxy(Class clazz)}
+ * + *

Possible Return Values

+ *
    + *
  • the target return type if it can be inferred
  • + *
  • the {@link Method#getReturnType() standard return type}, if + * the given {@code method} does not declare any {@link + * Method#getTypeParameters() generic types}
  • + *
  • the {@link Method#getReturnType() standard return type}, if the + * target return type cannot be inferred (e.g., due to type erasure)
  • + *
  • {@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
  • + *
+ * + * @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[] 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 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 getTypeVariableMap(Class clazz) { Reference> ref = typeVariableCache.get(clazz); 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 5514e0376c5..338529a4898 100644 --- a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java @@ -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)); + public void nullIfNotResolvable() { + GenericClass obj = new GenericClass(); + assertNull(resolveTypeArgument(obj.getClass(), GenericClass.class)); } @Test - public void testNullIfNotResolvable() { - GenericClass obj = new GenericClass(); - assertNull(GenericTypeResolver.resolveTypeArgument(obj.getClass(), GenericClass.class)); + 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 map = new HashMap(); + 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> { } - public abstract class MySuperclassType { } @@ -83,12 +147,70 @@ public class GenericTypeResolverTests { public class MyCollectionSuperclassType extends MySuperclassType> { } - public class MyTypeWithMethods { + public static class MyTypeWithMethods { public MyInterfaceType 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 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 createNamedProxy(String name, T object) { + return null; + } + + /** + * Simulates factory methods found in libraries such as Mockito and EasyMock. + */ + public static MOCK createMock(Class toMock) { + return null; + } + + /** + * Similar to {@link #createMock(Class)} but adds an additional method + * argument before the parameterized argument. + */ + public static T createNamedMock(String name, Class toMock) { + return null; + } + + /** + * Similar to {@link #createNamedMock(String, Class)} but adds an additional + * parameterized type. + */ + public static T createVMock(V name, Class 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 extractValueFrom(MyInterfaceType myInterfaceType) { + return null; + } + + /** + * Extract some magic value from the supplied map. + */ + public static V extractMagicValue(Map map) { + return null; + } + } static class GenericClass { diff --git a/spring-core/src/test/resources/log4j.xml b/spring-core/src/test/resources/log4j.xml index bd37e274728..c120c05811a 100644 --- a/spring-core/src/test/resources/log4j.xml +++ b/spring-core/src/test/resources/log4j.xml @@ -15,6 +15,10 @@ + + + +