From 8b1927f3ec0245130fe64719beae55c4e349c70d Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 14 Oct 2013 23:34:52 +0200 Subject: [PATCH] Fixed type prediction for generic factory methods We're consistently resolving class names now, and the entire algorithm moved from GenericTypeResolver to the internal AutowireUtils helper in the bean factory package. Issue: SPR-10411 --- .../AbstractAutowireCapableBeanFactory.java | 3 +- .../beans/factory/support/AutowireUtils.java | 111 +++++++++- .../factory/support/AutowireUtilsTests.java | 195 ++++++++++++++++++ .../core/GenericTypeResolverTests.java | 62 +----- 4 files changed, 305 insertions(+), 66 deletions(-) create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/support/AutowireUtilsTests.java 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 5636758042..7e4f33d0c2 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 @@ -665,7 +665,8 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac if (Modifier.isStatic(factoryMethod.getModifiers()) == isStatic && factoryMethod.getName().equals(mbd.getFactoryMethodName()) && factoryMethod.getParameterTypes().length >= minNrOfArgs) { - Class returnType = GenericTypeResolver.resolveReturnTypeForGenericMethod(factoryMethod, args); + Class returnType = AutowireUtils.resolveReturnTypeForFactoryMethod( + factoryMethod, args, getBeanClassLoader()); if (returnType != null) { returnTypes.add(returnType); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java index 140e11569c..d2d7577640 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -23,12 +23,16 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; import java.util.Arrays; import java.util.Comparator; import java.util.Set; import org.springframework.beans.factory.ObjectFactory; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** @@ -37,6 +41,7 @@ import org.springframework.util.ClassUtils; * * @author Juergen Hoeller * @author Mark Fisher + * @author Sam Brannen * @since 1.1.2 * @see AbstractAutowireCapableBeanFactory */ @@ -119,8 +124,8 @@ abstract class AutowireUtils { public static boolean isSetterDefinedInInterface(PropertyDescriptor pd, Set interfaces) { Method setter = pd.getWriteMethod(); if (setter != null) { - Class targetClass = setter.getDeclaringClass(); - for (Class ifc : interfaces) { + Class targetClass = setter.getDeclaringClass(); + for (Class ifc : interfaces) { if (ifc.isAssignableFrom(targetClass) && ClassUtils.hasMethod(ifc, setter.getName(), setter.getParameterTypes())) { return true; @@ -137,7 +142,7 @@ abstract class AutowireUtils { * @param requiredType the type to assign the result to * @return the resolved value */ - public static Object resolveAutowiringValue(Object autowiringValue, Class requiredType) { + public static Object resolveAutowiringValue(Object autowiringValue, Class requiredType) { if (autowiringValue instanceof ObjectFactory && !requiredType.isInstance(autowiringValue)) { ObjectFactory factory = (ObjectFactory) autowiringValue; if (autowiringValue instanceof Serializable && requiredType.isInterface()) { @@ -151,6 +156,104 @@ abstract class AutowireUtils { return autowiringValue; } + /** + * Determine the target type for the generic return type of the given + * generic factory method, where formal type variables are declared + * on the given method itself. + *

For example, given a factory method with the following signature, + * if {@code resolveReturnTypeForGenericMethod()} is invoked with the reflected + * method for {@code creatProxy()} and an {@code Object[]} array containing + * {@code MyService.class}, {@code resolveReturnTypeForGenericMethod()} will + * infer that the target return type is {@code MyService}. + *

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

Possible Return Values

+ * + * @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}) + * @param classLoader the ClassLoader to resolve class names against, if necessary + * (never {@code null}) + * @return the resolved target return type, the standard return type, or {@code null} + */ + public static Class resolveReturnTypeForFactoryMethod(Method method, Object[] args, ClassLoader classLoader) { + Assert.notNull(method, "Method must not be null"); + Assert.notNull(args, "Argument array must not be null"); + + TypeVariable[] declaredTypeVariables = method.getTypeParameters(); + Type genericReturnType = method.getGenericReturnType(); + Type[] methodArgumentTypes = method.getGenericParameterTypes(); + + // No declared type variables to inspect, so just return the standard return type. + if (declaredTypeVariables.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 < methodArgumentTypes.length) { + return null; + } + + // Ensure that the type variable (e.g., T) is declared directly on the method + // itself (e.g., via ), not on the enclosing class or interface. + boolean locallyDeclaredTypeVariableMatchesReturnType = false; + for (TypeVariable currentTypeVariable : declaredTypeVariables) { + if (currentTypeVariable.equals(genericReturnType)) { + locallyDeclaredTypeVariableMatchesReturnType = true; + break; + } + } + + if (locallyDeclaredTypeVariableMatchesReturnType) { + for (int i = 0; i < methodArgumentTypes.length; i++) { + Type currentMethodArgumentType = methodArgumentTypes[i]; + if (currentMethodArgumentType.equals(genericReturnType)) { + return args[i].getClass(); + } + if (currentMethodArgumentType instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) currentMethodArgumentType; + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + for (Type typeArg : actualTypeArguments) { + if (typeArg.equals(genericReturnType)) { + Object arg = args[i]; + if (arg instanceof Class) { + return (Class) arg; + } + else if (arg instanceof String) { + try { + return classLoader.loadClass((String) arg); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException( + "Could not resolve specified class name argument [" + arg + "]", ex); + } + } + else { + // Consider adding logic to determine the class of the typeArg, if possible. + // For now, just fall back... + return method.getReturnType(); + } + } + } + } + } + } + + // Fall back... + return method.getReturnType(); + } + /** * Reflective InvocationHandler for lazy access to the current target object. diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/AutowireUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/AutowireUtilsTests.java new file mode 100644 index 0000000000..9a834424d6 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/AutowireUtilsTests.java @@ -0,0 +1,195 @@ +/* + * Copyright 2002-2013 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.beans.factory.support; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.util.ReflectionUtils; + +import static org.junit.Assert.*; + +/** + * @author Juergen Hoeller + * @author Sam Brannen + */ +public class AutowireUtilsTests { + + @Test + public void genericMethodReturnTypes() { + Method notParameterized = ReflectionUtils.findMethod(MyTypeWithMethods.class, "notParameterized", new Class[]{}); + assertEquals(String.class, + AutowireUtils.resolveReturnTypeForFactoryMethod(notParameterized, new Object[]{}, getClass().getClassLoader())); + + Method notParameterizedWithArguments = ReflectionUtils.findMethod(MyTypeWithMethods.class, "notParameterizedWithArguments", + new Class[] { Integer.class, Boolean.class }); + assertEquals(String.class, + AutowireUtils.resolveReturnTypeForFactoryMethod(notParameterizedWithArguments, new Object[] { 99, true }, getClass().getClassLoader())); + + Method createProxy = ReflectionUtils.findMethod(MyTypeWithMethods.class, "createProxy", new Class[] { Object.class }); + assertEquals(String.class, + AutowireUtils.resolveReturnTypeForFactoryMethod(createProxy, new Object[] { "foo" }, getClass().getClassLoader())); + + Method createNamedProxyWithDifferentTypes = ReflectionUtils.findMethod(MyTypeWithMethods.class, "createNamedProxy", + new Class[] { String.class, Object.class }); + // one argument to few + assertNull( + AutowireUtils.resolveReturnTypeForFactoryMethod(createNamedProxyWithDifferentTypes, new Object[]{"enigma"}, getClass().getClassLoader())); + assertEquals(Long.class, + AutowireUtils.resolveReturnTypeForFactoryMethod(createNamedProxyWithDifferentTypes, new Object[] { "enigma", 99L }, getClass().getClassLoader())); + + Method createNamedProxyWithDuplicateTypes = ReflectionUtils.findMethod(MyTypeWithMethods.class, "createNamedProxy", + new Class[] { String.class, Object.class }); + assertEquals(String.class, + AutowireUtils.resolveReturnTypeForFactoryMethod(createNamedProxyWithDuplicateTypes, new Object[] { "enigma", "foo" }, getClass().getClassLoader())); + + Method createMock = ReflectionUtils.findMethod(MyTypeWithMethods.class, "createMock", new Class[] { Class.class }); + assertEquals(Runnable.class, + AutowireUtils.resolveReturnTypeForFactoryMethod(createMock, new Object[] { Runnable.class }, getClass().getClassLoader())); + assertEquals(Runnable.class, + AutowireUtils.resolveReturnTypeForFactoryMethod(createMock, new Object[] { Runnable.class.getName() }, getClass().getClassLoader())); + + Method createNamedMock = ReflectionUtils.findMethod(MyTypeWithMethods.class, "createNamedMock", new Class[] { String.class, + Class.class }); + assertEquals(Runnable.class, + AutowireUtils.resolveReturnTypeForFactoryMethod(createNamedMock, new Object[] { "foo", Runnable.class }, getClass().getClassLoader())); + + Method createVMock = ReflectionUtils.findMethod(MyTypeWithMethods.class, "createVMock", + new Class[] { Object.class, Class.class }); + assertEquals(Runnable.class, + AutowireUtils.resolveReturnTypeForFactoryMethod(createVMock, new Object[] { "foo", Runnable.class }, getClass().getClassLoader())); + + // Ideally we would expect String.class instead of Object.class, but + // resolveReturnTypeForFactoryMethod() does not currently support this form of + // look-up. + Method extractValueFrom = ReflectionUtils.findMethod(MyTypeWithMethods.class, "extractValueFrom", + new Class[] { MyInterfaceType.class }); + assertEquals(Object.class, + AutowireUtils.resolveReturnTypeForFactoryMethod(extractValueFrom, new Object[] { new MySimpleInterfaceType() }, getClass().getClassLoader())); + + // 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 = ReflectionUtils.findMethod(MyTypeWithMethods.class, "extractMagicValue", new Class[] { Map.class }); + assertEquals(Object.class, AutowireUtils.resolveReturnTypeForFactoryMethod(extractMagicValue, new Object[] { map }, getClass().getClassLoader())); + } + + + public interface MyInterfaceType { + } + + public class MySimpleInterfaceType implements MyInterfaceType { + } + + 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; + } + + public void readIntegerInputMessage(MyInterfaceType message) { + } + + public void readIntegerArrayInputMessage(MyInterfaceType[] message) { + } + + public void readGenericArrayInputMessage(T[] message) { + } + } + +} 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 53136acd9a..b7cb02c883 100644 --- a/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java +++ b/spring-core/src/test/java/org/springframework/core/GenericTypeResolverTests.java @@ -73,67 +73,6 @@ public class GenericTypeResolverTests { resolveReturnTypeArgument(findMethod(MyTypeWithMethods.class, "object"), MyInterfaceType.class)); } - /** - * @since 3.2 - */ - @Test - public void genericMethodReturnTypes() { - Method notParameterized = findMethod(MyTypeWithMethods.class, "notParameterized", new Class[] {}); - assertEquals(String.class, resolveReturnTypeForGenericMethod(notParameterized, new Object[] {})); - - Method notParameterizedWithArguments = findMethod(MyTypeWithMethods.class, "notParameterizedWithArguments", - new Class[] { Integer.class, Boolean.class }); - assertEquals(String.class, - resolveReturnTypeForGenericMethod(notParameterizedWithArguments, new Object[] { 99, true })); - - Method createProxy = findMethod(MyTypeWithMethods.class, "createProxy", new Class[] { Object.class }); - assertEquals(String.class, resolveReturnTypeForGenericMethod(createProxy, new Object[] { "foo" })); - - Method createNamedProxyWithDifferentTypes = findMethod(MyTypeWithMethods.class, "createNamedProxy", - new Class[] { String.class, Object.class }); - // one argument to few - assertNull(resolveReturnTypeForGenericMethod(createNamedProxyWithDifferentTypes, new Object[] { "enigma" })); - assertEquals(Long.class, - resolveReturnTypeForGenericMethod(createNamedProxyWithDifferentTypes, new Object[] { "enigma", 99L })); - - Method createNamedProxyWithDuplicateTypes = findMethod(MyTypeWithMethods.class, "createNamedProxy", - new Class[] { String.class, Object.class }); - assertEquals(String.class, - resolveReturnTypeForGenericMethod(createNamedProxyWithDuplicateTypes, new Object[] { "enigma", "foo" })); - - Method createMock = findMethod(MyTypeWithMethods.class, "createMock", new Class[] { Class.class }); - assertEquals(Runnable.class, resolveReturnTypeForGenericMethod(createMock, new Object[] { Runnable.class })); - - Method createNamedMock = findMethod(MyTypeWithMethods.class, "createNamedMock", new Class[] { String.class, - Class.class }); - assertEquals(Runnable.class, - resolveReturnTypeForGenericMethod(createNamedMock, new Object[] { "foo", Runnable.class })); - - Method createVMock = findMethod(MyTypeWithMethods.class, "createVMock", - new Class[] { Object.class, Class.class }); - assertEquals(Runnable.class, - resolveReturnTypeForGenericMethod(createVMock, new Object[] { "foo", Runnable.class })); - - // Ideally we would expect String.class instead of Object.class, but - // resolveReturnTypeForGenericMethod() does not currently support this form of - // look-up. - Method extractValueFrom = findMethod(MyTypeWithMethods.class, "extractValueFrom", - new Class[] { MyInterfaceType.class }); - assertEquals(Object.class, - resolveReturnTypeForGenericMethod(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, resolveReturnTypeForGenericMethod(extractMagicValue, new Object[] { map })); - } - - /** - * @since 3.2 - */ @Test public void testResolveType() { Method intMessageMethod = findMethod(MyTypeWithMethods.class, "readIntegerInputMessage", MyInterfaceType.class); @@ -197,6 +136,7 @@ public class GenericTypeResolverTests { assertThat(x, equalTo((Type) Long.class)); } + public interface MyInterfaceType { }