From f2cf78c5255e10d148c51ad9ee7e12c31aa01470 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 13 Apr 2022 17:27:05 -0700 Subject: [PATCH] Add programmatic autowiring support classes Add resolver utilities that can be used to perform programmatic autowiring of fields, methods, constructors and factory methods. The resolvers are designed to work in an AOT environment and allows the actual injection to be performed using functional interfaces. This allows leaner images to be created since `introspection` hints are required rather than full `invocation` hints. The resolvers also provide a reflection based fallback that can used when the functional interface cannot work. For example, a reflection based solution is required for private fields, methods and constructors. See gh-28414 --- .../annotation/AutowiredArguments.java | 87 ++ .../annotation/AutowiredElementResolver.java | 82 ++ .../AutowiredFieldValueResolver.java | 208 +++++ ...towiredInstantiationArgumentsResolver.java | 473 ++++++++++ .../AutowiredMethodArgumentsResolver.java | 221 +++++ .../AutowiredFieldValueResolverTests.java | 207 +++++ ...edInstantiationArgumentsResolverTests.java | 848 ++++++++++++++++++ ...AutowiredMethodArgumentsResolverTests.java | 228 +++++ 8 files changed, 2354 insertions(+) create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredArguments.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredElementResolver.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredFieldValueResolver.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredInstantiationArgumentsResolver.java create mode 100644 spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredMethodArgumentsResolver.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredFieldValueResolverTests.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredInstantiationArgumentsResolverTests.java create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredMethodArgumentsResolverTests.java diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredArguments.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredArguments.java new file mode 100644 index 00000000000..c7dfc573f75 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredArguments.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.annotation; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Resolved arguments to be autowired. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + * @see AutowiredInstantiationArgumentsResolver + * @see AutowiredMethodArgumentsResolver + */ +@FunctionalInterface +public interface AutowiredArguments { + + /** + * Return the resolved argument at the specified index. + * @param the type of the argument + * @param index the argument index + * @param requiredType the required argument type + * @return the argument + */ + @Nullable + @SuppressWarnings("unchecked") + default T get(int index, Class requiredType) { + Object value = get(index); + Assert.isInstanceOf(requiredType, value); + return (T) value; + } + + /** + * Return the resolved argument at the specified index. + * @param the type of the argument + * @param index the argument index + * @return the argument + */ + @Nullable + @SuppressWarnings("unchecked") + default T get(int index) { + return (T) toArray()[index]; + } + + /** + * Return the resolved argument at the specified index. + * @param index the argument index + * @return the argument + */ + default Object getObject(int index) { + return toArray()[index]; + } + + /** + * Return the arguments as an object array. + * @return the arguments as an object array + */ + Object[] toArray(); + + /** + * Factory method to create a new {@link AutowiredArguments} instance from + * the given object array. + * @param arguments the arguments + * @return a new {@link AutowiredArguments} instance + */ + static AutowiredArguments of(Object[] arguments) { + Assert.notNull(arguments, "Arguments must not be null"); + return () -> arguments; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredElementResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredElementResolver.java new file mode 100644 index 00000000000..c6d3990c16d --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredElementResolver.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.annotation; + +import java.util.Set; + +import javax.lang.model.element.Element; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.core.log.LogMessage; + +/** + * Base class for resolvers that support autowiring related to an + * {@link Element}. + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + */ +abstract class AutowiredElementResolver { + + private final Log logger = LogFactory.getLog(getClass()); + + protected final void registerDependentBeans(ConfigurableBeanFactory beanFactory, + String beanName, Set autowiredBeanNames) { + + for (String autowiredBeanName : autowiredBeanNames) { + if (beanFactory.containsBean(autowiredBeanName)) { + beanFactory.registerDependentBean(autowiredBeanName, beanName); + } + logger.trace(LogMessage.format( + "Autowiring by type from bean name %s' to bean named '%s'", beanName, + autowiredBeanName)); + } + } + + + /** + * {@link DependencyDescriptor} that supports shortcut bean resolution. + */ + @SuppressWarnings("serial") + static class ShortcutDependencyDescriptor extends DependencyDescriptor { + + private final String shortcut; + + private final Class requiredType; + + + public ShortcutDependencyDescriptor(DependencyDescriptor original, + String shortcut, Class requiredType) { + super(original); + this.shortcut = shortcut; + this.requiredType = requiredType; + } + + + @Override + public Object resolveShortcut(BeanFactory beanFactory) { + return beanFactory.getBean(this.shortcut, this.requiredType); + } + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredFieldValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredFieldValueResolver.java new file mode 100644 index 00000000000..d269202e869 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredFieldValueResolver.java @@ -0,0 +1,208 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.annotation; + +import java.lang.reflect.Field; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.InjectionPoint; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.function.ThrowingConsumer; + +/** + * Resolver used to support the autowiring of fields. Typically used in + * AOT-processed applications as a targeted alternative to the + * {@link org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + * AutowiredAnnotationBeanPostProcessor}. + *

+ * When resolving arguments in a native image, the {@link Field} being used must + * be marked with an {@link ExecutableMode#INTROSPECT introspection} hint so + * that field annotations can be read. Full {@link ExecutableMode#INVOKE + * invocation} hints are only required if the + * {@link #resolveAndSet(RegisteredBean, Object)} method of this class is being + * used (typically to support private fields). + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + */ +public final class AutowiredFieldValueResolver extends AutowiredElementResolver { + + private final String fieldName; + + private final boolean required; + + @Nullable + private final String shortcut; + + + private AutowiredFieldValueResolver(String fieldName, boolean required, + @Nullable String shortcut) { + + Assert.hasText(fieldName, "FieldName must not be empty"); + this.fieldName = fieldName; + this.required = required; + this.shortcut = shortcut; + } + + + /** + * Create a new {@link AutowiredFieldValueResolver} for the specified field + * where injection is optional. + * @param fieldName the field name + * @return a new {@link AutowiredFieldValueResolver} instance + */ + public static AutowiredFieldValueResolver forField(String fieldName) { + return new AutowiredFieldValueResolver(fieldName, false, null); + } + + /** + * Create a new {@link AutowiredFieldValueResolver} for the specified field + * where injection is required. + * @param fieldName the field name + * @return a new {@link AutowiredFieldValueResolver} instance + */ + public static AutowiredFieldValueResolver forRequiredField(String fieldName) { + return new AutowiredFieldValueResolver(fieldName, true, null); + } + + + /** + * Return a new {@link AutowiredFieldValueResolver} instance that uses a + * direct bean name injection shortcut. + * @param beanName the bean name to use as a shortcut + * @return a new {@link AutowiredFieldValueResolver} instance that uses the + * shortcuts + */ + public AutowiredFieldValueResolver withShortcut(String beanName) { + return new AutowiredFieldValueResolver(this.fieldName, this.required, beanName); + } + + /** + * Resolve the field for the specified registered bean and provide it to the + * given action. + * @param registeredBean the registered bean + * @param action the action to execute with the resolved field value + */ + public void resolve(RegisteredBean registeredBean, ThrowingConsumer action) { + Assert.notNull(registeredBean, "RegisteredBean must not be null"); + Assert.notNull(action, "Action must not be null"); + T resolved = resolve(registeredBean); + if (resolved != null) { + action.accept(resolved); + } + } + + /** + * Resolve the field value for the specified registered bean. + * @param registeredBean the registered bean + * @param requiredType the required type + * @return the resolved field value + */ + @Nullable + @SuppressWarnings("unchecked") + public T resolve(RegisteredBean registeredBean, Class requiredType) { + Object value = resolveObject(registeredBean); + Assert.isInstanceOf(requiredType, value); + return (T) value; + } + + /** + * Resolve the field value for the specified registered bean. + * @param registeredBean the registered bean + * @return the resolved field value + */ + @Nullable + @SuppressWarnings("unchecked") + public T resolve(RegisteredBean registeredBean) { + return (T) resolveObject(registeredBean); + } + + /** + * Resolve the field value for the specified registered bean. + * @param registeredBean the registered bean + * @return the resolved field value + */ + @Nullable + public Object resolveObject(RegisteredBean registeredBean) { + Assert.notNull(registeredBean, "RegisteredBean must not be null"); + return resolveValue(registeredBean, getField(registeredBean)); + } + + /** + * Resolve the field value for the specified registered bean and set it + * using reflection. + * @param registeredBean the registered bean + * @param instance the bean instance + */ + public void resolveAndSet(RegisteredBean registeredBean, Object instance) { + Assert.notNull(registeredBean, "RegisteredBean must not be null"); + Assert.notNull(instance, "Instance must not be null"); + Field field = getField(registeredBean); + Object resolved = resolveValue(registeredBean, field); + if (resolved != null) { + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, instance, resolved); + } + } + + @Nullable + private Object resolveValue(RegisteredBean registeredBean, Field field) { + String beanName = registeredBean.getBeanName(); + Class beanClass = registeredBean.getBeanClass(); + ConfigurableBeanFactory beanFactory = registeredBean.getBeanFactory(); + DependencyDescriptor descriptor = new DependencyDescriptor(field, this.required); + descriptor.setContainingClass(beanClass); + if (this.shortcut != null) { + descriptor = new ShortcutDependencyDescriptor(descriptor, this.shortcut, + field.getType()); + } + Set autowiredBeanNames = new LinkedHashSet<>(1); + TypeConverter typeConverter = beanFactory.getTypeConverter(); + try { + Assert.isInstanceOf(AutowireCapableBeanFactory.class, beanFactory); + Object value = ((AutowireCapableBeanFactory) beanFactory).resolveDependency( + descriptor, beanName, autowiredBeanNames, typeConverter); + registerDependentBeans(beanFactory, beanName, autowiredBeanNames); + return value; + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(null, beanName, + new InjectionPoint(field), ex); + } + } + + private Field getField(RegisteredBean registeredBean) { + Field field = ReflectionUtils.findField(registeredBean.getBeanClass(), + this.fieldName); + Assert.notNull(field, () -> "No field '" + this.fieldName + "' found on " + + registeredBean.getBeanClass().getName()); + return field; + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredInstantiationArgumentsResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredInstantiationArgumentsResolver.java new file mode 100644 index 00000000000..8abf1573078 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredInstantiationArgumentsResolver.java @@ -0,0 +1,473 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.annotation; + +import java.lang.reflect.Array; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.InjectionPoint; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionValueResolver; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.CollectionFactory; +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.function.ThrowingFunction; + +/** + * Resolver used to support the autowiring of constructors or factory methods. + * Typically used in AOT-processed applications as a targeted alternative to the + * reflection based injection. + *

+ * When resolving arguments in a native image, the {@link Constructor} or + * {@link Method} being used must be marked with an + * {@link ExecutableMode#INTROSPECT introspection} hint so that parameter + * annotations can be read. Full {@link ExecutableMode#INVOKE invocation} hints + * are only required if the {@code resolveAndInstantiate} methods of this class + * are being used (typically to support private constructors, methods or + * classes). + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + * @see AutowiredArguments + */ +public final class AutowiredInstantiationArgumentsResolver + extends AutowiredElementResolver { + + private final ExecutableLookup lookup; + + @Nullable + private final String[] shortcuts; + + + private AutowiredInstantiationArgumentsResolver(ExecutableLookup lookup, + @Nullable String[] shortcuts) { + + this.lookup = lookup; + this.shortcuts = shortcuts; + } + + + /** + * Create a {@link AutowiredInstantiationArgumentsResolver} that resolves + * arguments for the specified bean constructor. + * @param parameterTypes the constructor parameter types + * @return a new {@link AutowiredInstantiationArgumentsResolver} instance + */ + public static AutowiredInstantiationArgumentsResolver forConstructor( + Class... parameterTypes) { + + Assert.notNull(parameterTypes, "ParameterTypes must not be null"); + Assert.noNullElements(parameterTypes, + "ParameterTypes must not contain null elements"); + return new AutowiredInstantiationArgumentsResolver( + new ConstructorLookup(parameterTypes), null); + } + + /** + * Create a new {@link AutowiredInstantiationArgumentsResolver} that + * resolves arguments for the specified factory method. + * @param declaringClass the class that declares the factory method + * @param methodName the factory method name + * @param parameterTypes the factory method parameter types + * @return a new {@link AutowiredInstantiationArgumentsResolver} instance + */ + public static AutowiredInstantiationArgumentsResolver forFactoryMethod( + Class declaringClass, String methodName, Class... parameterTypes) { + + Assert.notNull(declaringClass, "DeclaringClass must not be null"); + Assert.hasText(methodName, "MethodName must not be empty"); + Assert.notNull(parameterTypes, "ParameterTypes must not be null"); + Assert.noNullElements(parameterTypes, + "ParameterTypes must not contain null elements"); + return new AutowiredInstantiationArgumentsResolver( + new FactoryMethodLookup(declaringClass, methodName, parameterTypes), + null); + } + + + ExecutableLookup getLookup() { + return this.lookup; + } + + /** + * Return a new {@link AutowiredInstantiationArgumentsResolver} instance + * that uses direct bean name injection shortcuts for specific parameters. + * @param beanNames the bean names to use as shortcuts (aligned with the + * constructor or factory method parameters) + * @return a new {@link AutowiredInstantiationArgumentsResolver} instance + * that uses the shortcuts + */ + public AutowiredInstantiationArgumentsResolver withShortcuts(String... beanNames) { + return new AutowiredInstantiationArgumentsResolver(this.lookup, beanNames); + } + + /** + * Resolve arguments for the specified registered bean and provide them to + * the given generator in order to return a result. + * @param registeredBean the registered bean + * @param generator the generator to execute with the resolved constructor + * or factory method arguments + */ + public T resolve(RegisteredBean registeredBean, + ThrowingFunction generator) { + + Assert.notNull(registeredBean, "RegisteredBean must not be null"); + Assert.notNull(generator, "Action must not be null"); + AutowiredArguments resolved = resolveArguments(registeredBean, + this.lookup.get(registeredBean)); + return generator.apply(resolved); + } + + /** + * Resolve arguments for the specified registered bean. + * @param registeredBean the registered bean + * @return the resolved constructor or factory method arguments + */ + public AutowiredArguments resolve(RegisteredBean registeredBean) { + Assert.notNull(registeredBean, "RegisteredBean must not be null"); + return resolveArguments(registeredBean, this.lookup.get(registeredBean)); + } + + /** + * Resolve arguments for the specified registered bean and instantiate a new + * instance using reflection. + * @param registeredBean the registered bean + * @return an instance of the bean + */ + @SuppressWarnings("unchecked") + public T resolveAndInstantiate(RegisteredBean registeredBean) { + return (T) resolveAndInstantiate(registeredBean, Object.class); + } + + /** + * Resolve arguments for the specified registered bean and instantiate a new + * instance using reflection. + * @param registeredBean the registered bean + * @param requiredType the required result type + * @return an instance of the bean + */ + @SuppressWarnings("unchecked") + public T resolveAndInstantiate(RegisteredBean registeredBean, + Class requiredType) { + + Assert.notNull(registeredBean, "RegisteredBean must not be null"); + Assert.notNull(registeredBean, "RequiredType must not be null"); + Executable executable = this.lookup.get(registeredBean); + AutowiredArguments arguments = resolveArguments(registeredBean, executable); + Object instance = instantiate(registeredBean.getBeanFactory(), executable, + arguments.toArray()); + Assert.isInstanceOf(requiredType, instance); + return (T) instance; + } + + private AutowiredArguments resolveArguments(RegisteredBean registeredBean, + Executable executable) { + + Assert.isInstanceOf(AbstractAutowireCapableBeanFactory.class, + registeredBean.getBeanFactory()); + String beanName = registeredBean.getBeanName(); + Class beanClass = registeredBean.getBeanClass(); + AbstractAutowireCapableBeanFactory beanFactory = (AbstractAutowireCapableBeanFactory) registeredBean + .getBeanFactory(); + RootBeanDefinition mergedBeanDefinition = registeredBean + .getMergedBeanDefinition(); + int startIndex = (executable instanceof Constructor constructor + && ClassUtils.isInnerClass(constructor.getDeclaringClass())) ? 1 : 0; + int parameterCount = executable.getParameterCount(); + Object[] resolved = new Object[parameterCount - startIndex]; + Assert.isTrue(this.shortcuts == null || this.shortcuts.length == resolved.length, + () -> "'shortcuts' must contain " + resolved.length + " elements"); + Set autowiredBeans = new LinkedHashSet<>(resolved.length); + ConstructorArgumentValues argumentValues = resolveArgumentValues(beanFactory, + beanName, mergedBeanDefinition); + for (int i = startIndex; i < parameterCount; i++) { + MethodParameter parameter = getMethodParameter(executable, i); + DependencyDescriptor dependencyDescriptor = new DependencyDescriptor( + parameter, true); + String shortcut = (this.shortcuts != null) ? this.shortcuts[i - startIndex] + : null; + if (shortcut != null) { + dependencyDescriptor = new ShortcutDependencyDescriptor( + dependencyDescriptor, shortcut, beanClass); + } + ValueHolder argumentValue = argumentValues.getIndexedArgumentValue(i, null); + resolved[i - startIndex] = resolveArgument(beanFactory, beanName, + autowiredBeans, parameter, dependencyDescriptor, argumentValue); + } + registerDependentBeans(beanFactory, beanName, autowiredBeans); + if (executable instanceof Method method) { + mergedBeanDefinition.setResolvedFactoryMethod(method); + } + return AutowiredArguments.of(resolved); + } + + private MethodParameter getMethodParameter(Executable executable, int index) { + if (executable instanceof Constructor constructor) { + return new MethodParameter(constructor, index); + } + if (executable instanceof Method method) { + return new MethodParameter(method, index); + } + throw new IllegalStateException( + "Unsupported executable " + executable.getClass().getName()); + } + + private ConstructorArgumentValues resolveArgumentValues( + AbstractAutowireCapableBeanFactory beanFactory, String beanName, + RootBeanDefinition mergedBeanDefinition) { + + ConstructorArgumentValues resolved = new ConstructorArgumentValues(); + if (mergedBeanDefinition.hasConstructorArgumentValues()) { + BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver( + beanFactory, beanName, mergedBeanDefinition, + beanFactory.getTypeConverter()); + ConstructorArgumentValues values = mergedBeanDefinition + .getConstructorArgumentValues(); + values.getIndexedArgumentValues().forEach((index, valueHolder) -> { + ValueHolder resolvedValue = resolveArgumentValue(valueResolver, + valueHolder); + resolved.addIndexedArgumentValue(index, resolvedValue); + }); + } + return resolved; + } + + private ValueHolder resolveArgumentValue(BeanDefinitionValueResolver resolver, + ValueHolder valueHolder) { + + if (valueHolder.isConverted()) { + return valueHolder; + } + Object resolvedValue = resolver.resolveValueIfNecessary("constructor argument", + valueHolder.getValue()); + ValueHolder resolvedValueHolder = new ValueHolder(resolvedValue, + valueHolder.getType(), valueHolder.getName()); + resolvedValueHolder.setSource(valueHolder); + return resolvedValueHolder; + } + + @Nullable + private Object resolveArgument(AbstractAutowireCapableBeanFactory beanFactory, + String beanName, Set autowiredBeans, MethodParameter parameter, + DependencyDescriptor dependencyDescriptor, + @Nullable ValueHolder argumentValue) { + + TypeConverter typeConverter = beanFactory.getTypeConverter(); + Class parameterType = parameter.getParameterType(); + if (argumentValue != null) { + return (!argumentValue.isConverted()) ? typeConverter + .convertIfNecessary(argumentValue.getValue(), parameterType) + : argumentValue.getConvertedValue(); + } + try { + try { + return beanFactory.resolveDependency(dependencyDescriptor, beanName, + autowiredBeans, typeConverter); + } + catch (NoSuchBeanDefinitionException ex) { + if (parameterType.isArray()) { + return Array.newInstance(parameterType.getComponentType(), 0); + } + if (CollectionFactory.isApproximableCollectionType(parameterType)) { + return CollectionFactory.createCollection(parameterType, 0); + } + if (CollectionFactory.isApproximableMapType(parameterType)) { + return CollectionFactory.createMap(parameterType, 0); + } + throw ex; + } + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(null, beanName, + new InjectionPoint(parameter), ex); + } + } + + private Object instantiate(ConfigurableBeanFactory beanFactory, Executable executable, + Object[] arguments) { + + try { + if (executable instanceof Constructor constructor) { + return instantiate(constructor, arguments); + } + if (executable instanceof Method method) { + return instantiate(beanFactory, method, arguments); + } + } + catch (Exception ex) { + throw new BeanCreationException( + "Unable to instantiate bean using " + executable, ex); + } + throw new IllegalStateException( + "Unsupported executable " + executable.getClass().getName()); + } + + private Object instantiate(Constructor constructor, Object[] arguments) + throws Exception { + + Class declaringClass = constructor.getDeclaringClass(); + if (ClassUtils.isInnerClass(declaringClass)) { + Object enclosingInstance = createInstance(declaringClass.getEnclosingClass()); + arguments = ObjectUtils.addObjectToArray(arguments, enclosingInstance, 0); + } + ReflectionUtils.makeAccessible(constructor); + return constructor.newInstance(arguments); + } + + private Object instantiate(ConfigurableBeanFactory beanFactory, Method method, + Object[] arguments) { + + ReflectionUtils.makeAccessible(method); + Object target = getFactoryMethodTarget(beanFactory, method); + return ReflectionUtils.invokeMethod(method, target, arguments); + } + + @Nullable + private Object getFactoryMethodTarget(BeanFactory beanFactory, Method method) { + if (Modifier.isStatic(method.getModifiers())) { + return null; + } + Class declaringClass = method.getDeclaringClass(); + return beanFactory.getBean(declaringClass); + } + + private Object createInstance(Class clazz) throws Exception { + if (!ClassUtils.isInnerClass(clazz)) { + Constructor constructor = clazz.getDeclaredConstructor(); + ReflectionUtils.makeAccessible(constructor); + return constructor.newInstance(); + } + Class enclosingClass = clazz.getEnclosingClass(); + Constructor constructor = clazz.getDeclaredConstructor(enclosingClass); + return constructor.newInstance(createInstance(enclosingClass)); + } + + + /** + * Performs lookup of the {@link Executable}. + */ + static abstract class ExecutableLookup { + + abstract Executable get(RegisteredBean registeredBean); + + final String toCommaSeparatedNames(Class... parameterTypes) { + return Arrays.stream(parameterTypes).map(Class::getName) + .collect(Collectors.joining(", ")); + } + + } + + + /** + * Performs lookup of the {@link Constructor}. + */ + private static class ConstructorLookup extends ExecutableLookup { + + private final Class[] parameterTypes; + + ConstructorLookup(Class[] parameterTypes) { + this.parameterTypes = parameterTypes; + } + + @Override + public Executable get(RegisteredBean registeredBean) { + Class beanClass = registeredBean.getBeanClass(); + try { + Class[] actualParameterTypes = (!ClassUtils.isInnerClass(beanClass)) + ? this.parameterTypes : ObjectUtils.addObjectToArray( + this.parameterTypes, beanClass.getEnclosingClass(), 0); + return beanClass.getDeclaredConstructor(actualParameterTypes); + } + catch (NoSuchMethodException ex) { + throw new IllegalArgumentException(String.format( + "%s cannot be found on %s", this, beanClass.getName()), ex); + } + } + + @Override + public String toString() { + return String.format("Constructor with parameter types [%s]", + toCommaSeparatedNames(this.parameterTypes)); + } + + } + + + /** + * Performs lookup of the factory {@link Method}. + */ + private static class FactoryMethodLookup extends ExecutableLookup { + + private final Class declaringClass; + + private final String methodName; + + private final Class[] parameterTypes; + + + FactoryMethodLookup(Class declaringClass, String methodName, + Class[] parameterTypes) { + this.declaringClass = declaringClass; + this.methodName = methodName; + this.parameterTypes = parameterTypes; + } + + + @Override + public Executable get(RegisteredBean registeredBean) { + Method method = ReflectionUtils.findMethod(this.declaringClass, + this.methodName, this.parameterTypes); + Assert.notNull(method, () -> String.format("%s cannot be found", this)); + return method; + } + + @Override + public String toString() { + return String.format( + "Factory method '%s' with parameter types [%s] declared on %s", + this.methodName, toCommaSeparatedNames(this.parameterTypes), + this.declaringClass); + } + + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredMethodArgumentsResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredMethodArgumentsResolver.java new file mode 100644 index 00000000000..1170c0f133e --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredMethodArgumentsResolver.java @@ -0,0 +1,221 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.annotation; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.InjectionPoint; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.function.ThrowingConsumer; + +/** + * Resolver used to support the autowiring of methods. Typically used in + * AOT-processed applications as a targeted alternative to the + * {@link org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + * AutowiredAnnotationBeanPostProcessor}. + *

+ * When resolving arguments in a native image, the {@link Method} being used + * must be marked with an {@link ExecutableMode#INTROSPECT introspection} hint + * so that field annotations can be read. Full {@link ExecutableMode#INVOKE + * invocation} hints are only required if the + * {@link #resolveAndInvoke(RegisteredBean, Object)} method of this class is + * being used (typically to support private methods). + * + * @author Phillip Webb + * @author Stephane Nicoll + * @since 6.0 + */ +public final class AutowiredMethodArgumentsResolver extends AutowiredElementResolver { + + private final String methodName; + + private final Class[] parameterTypes; + + private final boolean required; + + @Nullable + private final String[] shortcuts; + + + private AutowiredMethodArgumentsResolver(String methodName, Class[] parameterTypes, + boolean required, @Nullable String[] shortcuts) { + + Assert.hasText(methodName, "MethodName must not be empty"); + this.methodName = methodName; + this.parameterTypes = parameterTypes; + this.required = required; + this.shortcuts = shortcuts; + } + + /** + * Create a new {@link AutowiredMethodArgumentsResolver} for the specified + * method where injection is optional. + * @param methodName the method name + * @param parameterTypes the factory method parameter types + * @return a new {@link AutowiredFieldValueResolver} instance + */ + public static AutowiredMethodArgumentsResolver forMethod(String methodName, + Class... parameterTypes) { + + return new AutowiredMethodArgumentsResolver(methodName, parameterTypes, false, + null); + } + + /** + * Create a new {@link AutowiredMethodArgumentsResolver} for the specified + * method where injection is required. + * @param methodName the method name + * @param parameterTypes the factory method parameter types + * @return a new {@link AutowiredFieldValueResolver} instance + */ + public static AutowiredMethodArgumentsResolver forRequiredMethod(String methodName, + Class... parameterTypes) { + + return new AutowiredMethodArgumentsResolver(methodName, parameterTypes, true, + null); + } + + /** + * Return a new {@link AutowiredInstantiationArgumentsResolver} instance + * that uses direct bean name injection shortcuts for specific parameters. + * @param beanNames the bean names to use as shortcuts (aligned with the + * method parameters) + * @return a new {@link AutowiredMethodArgumentsResolver} instance that uses + * the shortcuts + */ + public AutowiredMethodArgumentsResolver withShortcut(String... beanNames) { + return new AutowiredMethodArgumentsResolver(this.methodName, this.parameterTypes, + this.required, beanNames); + } + + /** + * Resolve the method arguments for the specified registered bean and + * provide it to the given action. + * @param registeredBean the registered bean + * @param action the action to execute with the resolved method arguments + */ + public void resolve(RegisteredBean registeredBean, + ThrowingConsumer action) { + + Assert.notNull(registeredBean, "RegisteredBean must not be null"); + Assert.notNull(action, "Action must not be null"); + AutowiredArguments resolved = resolve(registeredBean); + if (resolved != null) { + action.accept(resolved); + } + } + + /** + * Resolve the method arguments for the specified registered bean. + * @param registeredBean the registered bean + * @return the resolved method arguments + */ + @Nullable + public AutowiredArguments resolve(RegisteredBean registeredBean) { + Assert.notNull(registeredBean, "RegisteredBean must not be null"); + return resolveArguments(registeredBean, getMethod(registeredBean)); + } + + /** + * Resolve the method arguments for the specified registered bean and invoke + * the method using reflection. + * @param registeredBean the registered bean + * @param instance the bean instance + */ + public void resolveAndInvoke(RegisteredBean registeredBean, Object instance) { + Assert.notNull(registeredBean, "RegisteredBean must not be null"); + Assert.notNull(instance, "Instance must not be null"); + Method method = getMethod(registeredBean); + AutowiredArguments resolved = resolveArguments(registeredBean, method); + if (resolved != null) { + ReflectionUtils.makeAccessible(method); + ReflectionUtils.invokeMethod(method, instance, resolved.toArray()); + } + } + + @Nullable + private AutowiredArguments resolveArguments(RegisteredBean registeredBean, + Method method) { + + String beanName = registeredBean.getBeanName(); + Class beanClass = registeredBean.getBeanClass(); + ConfigurableBeanFactory beanFactory = registeredBean.getBeanFactory(); + Assert.isInstanceOf(AutowireCapableBeanFactory.class, beanFactory); + AutowireCapableBeanFactory autowireCapableBeanFactory = (AutowireCapableBeanFactory) beanFactory; + int argumentCount = method.getParameterCount(); + Object[] arguments = new Object[argumentCount]; + Set autowiredBeanNames = new LinkedHashSet<>(argumentCount); + TypeConverter typeConverter = beanFactory.getTypeConverter(); + for (int i = 0; i < argumentCount; i++) { + MethodParameter parameter = new MethodParameter(method, i); + DependencyDescriptor descriptor = new DependencyDescriptor(parameter, + this.required); + descriptor.setContainingClass(beanClass); + String shortcut = (this.shortcuts != null) ? this.shortcuts[i] : null; + if (shortcut != null) { + descriptor = new ShortcutDependencyDescriptor(descriptor, shortcut, + parameter.getParameterType()); + } + try { + Object argument = autowireCapableBeanFactory.resolveDependency(descriptor, + beanName, autowiredBeanNames, typeConverter); + if (argument == null && !this.required) { + return null; + } + arguments[i] = argument; + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(null, beanName, + new InjectionPoint(parameter), ex); + } + } + registerDependentBeans(beanFactory, beanName, autowiredBeanNames); + return AutowiredArguments.of(arguments); + } + + private Method getMethod(RegisteredBean registeredBean) { + Method method = ReflectionUtils.findMethod(registeredBean.getBeanClass(), + this.methodName, this.parameterTypes); + Assert.notNull(method, + () -> String.format( + "Method '%s' with parameter types [%s] declared on %s", + this.methodName, toCommaSeparatedNames(this.parameterTypes), + registeredBean.getBeanClass().getName())); + return method; + } + + private String toCommaSeparatedNames(Class... parameterTypes) { + return Arrays.stream(parameterTypes).map(Class::getName) + .collect(Collectors.joining(", ")); + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredFieldValueResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredFieldValueResolverTests.java new file mode 100644 index 00000000000..0e8f60d8e64 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredFieldValueResolverTests.java @@ -0,0 +1,207 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.annotation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.util.function.ThrowingConsumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link AutowiredFieldValueResolver}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +class AutowiredFieldValueResolverTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Test + void forFieldWhenFieldNameIsEmptyThrowsException() { + String message = "FieldName must not be empty"; + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forField(null)) + .withMessage(message); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forField("")) + .withMessage(message); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forRequiredField(null)) + .withMessage(message); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forRequiredField(" ")) + .withMessage(message); + } + + @Test + void resolveWhenRegisteredBeanIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy( + () -> AutowiredFieldValueResolver.forField("string").resolve(null)) + .withMessage("RegisteredBean must not be null"); + } + + @Test + void resolveWhenFieldIsMissingThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forField("missing") + .resolve(registeredBean)) + .withMessage("No field 'missing' found on " + TestBean.class.getName()); + } + + @Test + void resolveReturnsValue() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = AutowiredFieldValueResolver.forField("string") + .resolve(registeredBean); + assertThat(resolved).isEqualTo("1"); + } + + @Test + void resolveWhenRequiredFieldAndBeanReturnsValue() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = AutowiredFieldValueResolver.forRequiredField("string") + .resolve(registeredBean); + assertThat(resolved).isEqualTo("1"); + } + + @Test + void resolveWhenRequiredFieldAndNoBeanReturnsNull() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + Object resolved = AutowiredFieldValueResolver.forField("string") + .resolve(registeredBean); + assertThat(resolved).isNull(); + } + + @Test + void resolveWhenRequiredFieldAndNoBeanThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredFieldValueResolver resolver = AutowiredFieldValueResolver + .forRequiredField("string"); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> resolver.resolve(registeredBean)).satisfies(ex -> { + assertThat(ex.getBeanName()).isEqualTo("testBean"); + assertThat(ex.getInjectionPoint()).isNotNull(); + assertThat(ex.getInjectionPoint().getField().getName()) + .isEqualTo("string"); + }); + } + + @Test + void resolveAndSetWhenInstanceIsNullThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forField("string") + .resolveAndSet(registeredBean, null)) + .withMessage("Instance must not be null"); + } + + @Test + void resolveAndSetSetsValue() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + TestBean testBean = new TestBean(); + AutowiredFieldValueResolver.forField("string").resolveAndSet(registeredBean, + testBean); + assertThat(testBean).extracting("string").isEqualTo("1"); + } + + @Test + void resolveWithActionWhenActionIsNullThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredFieldValueResolver.forField("string") + .resolve(registeredBean, (ThrowingConsumer) null)) + .withMessage("Action must not be null"); + } + + @Test + void resolveWithActionCallsAction() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + List result = new ArrayList<>(); + AutowiredFieldValueResolver.forField("string").resolve(registeredBean, + result::add); + assertThat(result).containsExactly("1"); + } + + @Test + void resolveWithActionWhenDeducedGenericCallsAction() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + TestBean testBean = new TestBean(); + testBean.string = AutowiredFieldValueResolver.forField("string") + .resolve(registeredBean); + } + + @Test + void resolveObjectWhenUsingShortcutInjectsDirectly() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory() { + + @Override + protected Map findAutowireCandidates(String beanName, + Class requiredType, DependencyDescriptor descriptor) { + throw new AssertionError("Should be shortcut"); + } + + }; + beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(beanFactory); + AutowiredFieldValueResolver resolver = AutowiredFieldValueResolver + .forField("string"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> resolver.resolve(registeredBean)); + assertThat(resolver.withShortcut("one").resolveObject(registeredBean)) + .isEqualTo("1"); + } + + @Test + void resolveRegistersDependantBeans() { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredFieldValueResolver.forField("string").resolve(registeredBean); + assertThat(this.beanFactory.getDependentBeans("one")).containsExactly("testBean"); + } + + private RegisteredBean registerTestBean(DefaultListableBeanFactory beanFactory) { + beanFactory.registerBeanDefinition("testBean", + new RootBeanDefinition(TestBean.class)); + return RegisteredBean.of(beanFactory, "testBean"); + } + + static class TestBean { + + String string; + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredInstantiationArgumentsResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredInstantiationArgumentsResolverTests.java new file mode 100644 index 00000000000..255309676e2 --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredInstantiationArgumentsResolverTests.java @@ -0,0 +1,848 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.annotation; + +import java.io.InputStream; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Executable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.AnnotationConsumer; + +import org.springframework.beans.factory.BeanCurrentlyInCreationException; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.annotation.AutowiredInstantiationArgumentsResolverTests.Enclosing.InnerSingleArgConstructor; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.env.Environment; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AutowiredInstantiationArgumentsResolver}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +class AutowiredInstantiationArgumentsResolverTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Test + void forConstructorWhenParameterTypesIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredInstantiationArgumentsResolver + .forConstructor((Class[]) null)) + .withMessage("ParameterTypes must not be null"); + } + + @Test + void forConstructorWhenParameterTypesContainsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredInstantiationArgumentsResolver + .forConstructor(String.class, null)) + .withMessage("ParameterTypes must not contain null elements"); + } + + @Test + void forConstructorWhenNotFoundThrowsException() { + AutowiredInstantiationArgumentsResolver resolver = AutowiredInstantiationArgumentsResolver + .forConstructor(InputStream.class); + Source source = new Source(SingleArgConstructor.class, resolver); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> resolver.resolve(registerBean)).withMessage( + "Constructor with parameter types [java.io.InputStream] cannot be found on " + + SingleArgConstructor.class.getName()); + } + + @Test + void forFactoryMethodWhenDeclaringClassIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredInstantiationArgumentsResolver + .forFactoryMethod(null, "test")) + .withMessage("DeclaringClass must not be null"); + } + + @Test + void forFactoryMethodWhenNameIsEmptyThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredInstantiationArgumentsResolver + .forFactoryMethod(SingleArgFactory.class, "")) + .withMessage("MethodName must not be empty"); + } + + @Test + void forFactoryMethodWhenParameterTypesIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> AutowiredInstantiationArgumentsResolver.forFactoryMethod( + SingleArgFactory.class, "single", (Class[]) null)) + .withMessage("ParameterTypes must not be null"); + } + + @Test + void forFactoryMethodWhenParameterTypesContainsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> AutowiredInstantiationArgumentsResolver.forFactoryMethod( + SingleArgFactory.class, "single", String.class, null)) + .withMessage("ParameterTypes must not contain null elements"); + } + + @Test + void forFactoryMethodWhenNotFoundThrowsException() { + AutowiredInstantiationArgumentsResolver resolver = AutowiredInstantiationArgumentsResolver + .forFactoryMethod(SingleArgFactory.class, "single", InputStream.class); + Source source = new Source(String.class, resolver); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> resolver.resolve(registerBean)).withMessage( + "Factory method 'single' with parameter types [java.io.InputStream] declared on class " + + SingleArgFactory.class.getName() + " cannot be found"); + } + + @Test + void resolveWithActionWhenActionIsNullThrowsException() { + AutowiredInstantiationArgumentsResolver resolver = AutowiredInstantiationArgumentsResolver + .forConstructor(); + Source source = new Source(NoArgConstructor.class, resolver); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> resolver.resolve(registerBean, null)) + .withMessage("Action must not be null"); + } + + @Test + void resolveWithActionCallsAction() { + AutowiredInstantiationArgumentsResolver resolver = AutowiredInstantiationArgumentsResolver + .forConstructor(String.class); + Source source = new Source(SingleArgConstructor.class, resolver); + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + List result = new ArrayList<>(); + resolver.resolve(registerBean, result::add); + assertThat(result).hasSize(1); + assertThat(((AutowiredArguments) result.get(0)).toArray()).containsExactly("1"); + } + + @Test + void resolveWhenRegisteredBeanIsNullThrowsException() { + AutowiredInstantiationArgumentsResolver resolver = AutowiredInstantiationArgumentsResolver + .forConstructor(String.class); + assertThatIllegalArgumentException().isThrownBy(() -> resolver.resolve(null)) + .withMessage("RegisteredBean must not be null"); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveAndInstantiate(Source source) { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("testFactory", new SingleArgFactory()); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + Object instance = source.getResolver().resolveAndInstantiate(registerBean); + if (instance instanceof SingleArgConstructor singleArgConstructor) { + instance = singleArgConstructor.getString(); + } + assertThat(instance).isEqualTo("1"); + } + + @ParameterizedResolverTest(Sources.INNER_CLASS_SINGLE_ARG) + void resolveAndInstantiateNested(Source source) { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("testFactory", + new Enclosing().new InnerSingleArgFactory()); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + Object instance = source.getResolver().resolveAndInstantiate(registerBean); + if (instance instanceof InnerSingleArgConstructor innerSingleArgConstructor) { + instance = innerSingleArgConstructor.getString(); + } + assertThat(instance).isEqualTo("1"); + } + + @Test + void resolveNoArgConstructor() { + RootBeanDefinition beanDefinition = new RootBeanDefinition( + NoArgConstructor.class); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + RegisteredBean registeredBean = RegisteredBean.of(this.beanFactory, "test"); + AutowiredArguments resolved = AutowiredInstantiationArgumentsResolver + .forConstructor().resolve(registeredBean); + assertThat(resolved.toArray()).isEmpty(); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveSingleArgConstructor(Source source) { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = source.registerBean(this.beanFactory); + assertThat(source.getResolver().resolve(registeredBean).toArray()) + .containsExactly("1"); + } + + @ParameterizedResolverTest(Sources.INNER_CLASS_SINGLE_ARG) + void resolvedNestedSingleArgConstructor(Source source) { + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = source.registerBean(this.beanFactory); + assertThat(source.getResolver().resolve(registeredBean).toArray()) + .containsExactly("1"); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveRequiredDependencyNotPresentThrowsUnsatisfiedDependencyException( + Source source) { + RegisteredBean registeredBean = source.registerBean(this.beanFactory); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> source.getResolver().resolve(registeredBean)) + .satisfies(ex -> { + assertThat(ex.getBeanName()).isEqualTo("testBean"); + assertThat(ex.getInjectionPoint()).isNotNull(); + assertThat(ex.getInjectionPoint().getMember()) + .isEqualTo(source.lookupExecutable(registeredBean)); + }); + } + + @Test + void resolveInInstanceSupplierWithSelfReferenceThrowsException() { + // SingleArgFactory.single(...) expects a String to be injected + // and our own bean is a String so it's a valid candidate + this.beanFactory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor()); + RootBeanDefinition beanDefinition = new RootBeanDefinition(String.class); + beanDefinition.setInstanceSupplier(InstanceSupplier.of(registeredBean -> { + AutowiredArguments args = AutowiredInstantiationArgumentsResolver + .forFactoryMethod(SingleArgFactory.class, "single", String.class) + .resolve(registeredBean); + return new SingleArgFactory().single((String) args.get(0)); + })); + this.beanFactory.registerBeanDefinition("test", beanDefinition); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> this.beanFactory.getBean("test")); + } + + @ParameterizedResolverTest(Sources.ARRAY_OF_BEANS) + void resolveArrayOfBeans(Source source) { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((Object[]) arguments.get(0)).containsExactly("1", "2"); + } + + @ParameterizedResolverTest(Sources.ARRAY_OF_BEANS) + void resolveRequiredArrayOfBeansInjectEmptyArray(Source source) { + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((Object[]) arguments.get(0)).isEmpty(); + } + + @ParameterizedResolverTest(Sources.LIST_OF_BEANS) + void resolveListOfBeans(Source source) { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isInstanceOf(List.class).asList() + .containsExactly("1", "2"); + } + + @ParameterizedResolverTest(Sources.LIST_OF_BEANS) + void resolveRequiredListOfBeansInjectEmptyList(Source source) { + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((List) arguments.get(0)).isEmpty(); + } + + @ParameterizedResolverTest(Sources.SET_OF_BEANS) + @SuppressWarnings("unchecked") + void resolveSetOfBeans(Source source) { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((Set) arguments.get(0)).containsExactly("1", "2"); + } + + @ParameterizedResolverTest(Sources.SET_OF_BEANS) + void resolveRequiredSetOfBeansInjectEmptySet(Source source) { + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((Set) arguments.get(0)).isEmpty(); + } + + @ParameterizedResolverTest(Sources.MAP_OF_BEANS) + @SuppressWarnings("unchecked") + void resolveMapOfBeans(Source source) { + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((Map) arguments.get(0)) + .containsExactly(entry("one", "1"), entry("two", "2")); + } + + @ParameterizedResolverTest(Sources.MAP_OF_BEANS) + void resolveRequiredMapOfBeansInjectEmptySet(Source source) { + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat((Map) arguments.get(0)).isEmpty(); + } + + @ParameterizedResolverTest(Sources.MULTI_ARGS) + void resolveMultiArgsConstructor(Source source) { + ResourceLoader resourceLoader = new DefaultResourceLoader(); + Environment environment = mock(Environment.class); + this.beanFactory.registerResolvableDependency(ResourceLoader.class, + resourceLoader); + this.beanFactory.registerSingleton("environment", environment); + this.beanFactory.registerSingleton("one", "1"); + RegisteredBean registerBean = source.registerBean(this.beanFactory); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(3); + assertThat(arguments.getObject(0)).isEqualTo(resourceLoader); + assertThat(arguments.getObject(1)).isEqualTo(environment); + assertThat(((ObjectProvider) arguments.get(2)).getIfAvailable()) + .isEqualTo("1"); + } + + @ParameterizedResolverTest(Sources.MIXED_ARGS) + void resolveMixedArgsConstructorWithUserValue(Source source) { + ResourceLoader resourceLoader = new DefaultResourceLoader(); + Environment environment = mock(Environment.class); + this.beanFactory.registerResolvableDependency(ResourceLoader.class, + resourceLoader); + this.beanFactory.registerSingleton("environment", environment); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> { + beanDefinition + .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + beanDefinition.getConstructorArgumentValues() + .addIndexedArgumentValue(1, "user-value"); + }); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(3); + assertThat(arguments.getObject(0)).isEqualTo(resourceLoader); + assertThat(arguments.getObject(1)).isEqualTo("user-value"); + assertThat(arguments.getObject(2)).isEqualTo(environment); + } + + @ParameterizedResolverTest(Sources.MIXED_ARGS) + void resolveMixedArgsConstructorWithUserBeanReference(Source source) { + ResourceLoader resourceLoader = new DefaultResourceLoader(); + Environment environment = mock(Environment.class); + this.beanFactory.registerResolvableDependency(ResourceLoader.class, + resourceLoader); + this.beanFactory.registerSingleton("environment", environment); + this.beanFactory.registerSingleton("one", "1"); + this.beanFactory.registerSingleton("two", "2"); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> { + beanDefinition + .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + beanDefinition.getConstructorArgumentValues() + .addIndexedArgumentValue(1, new RuntimeBeanReference("two")); + }); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(3); + assertThat(arguments.getObject(0)).isEqualTo(resourceLoader); + assertThat(arguments.getObject(1)).isEqualTo("2"); + assertThat(arguments.getObject(2)).isEqualTo(environment); + } + + @Test + void resolveUserValueWithTypeConversionRequired() { + Source source = new Source(CharDependency.class, + AutowiredInstantiationArgumentsResolver.forConstructor(char.class)); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> { + beanDefinition + .setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + beanDefinition.getConstructorArgumentValues() + .addIndexedArgumentValue(0, "\\"); + }); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isInstanceOf(Character.class).isEqualTo('\\'); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveUserValueWithBeanReference(Source source) { + this.beanFactory.registerSingleton("stringBean", "string"); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> beanDefinition.getConstructorArgumentValues() + .addIndexedArgumentValue(0, + new RuntimeBeanReference("stringBean"))); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isEqualTo("string"); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveUserValueWithBeanDefinition(Source source) { + AbstractBeanDefinition userValue = BeanDefinitionBuilder + .rootBeanDefinition(String.class, () -> "string").getBeanDefinition(); + RegisteredBean registerBean = source.registerBean(this.beanFactory, + beanDefinition -> beanDefinition.getConstructorArgumentValues() + .addIndexedArgumentValue(0, userValue)); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isEqualTo("string"); + } + + @ParameterizedResolverTest(Sources.SINGLE_ARG) + void resolveUserValueThatIsAlreadyResolved(Source source) { + RegisteredBean registerBean = source.registerBean(this.beanFactory); + BeanDefinition mergedBeanDefinition = this.beanFactory + .getMergedBeanDefinition("testBean"); + ValueHolder valueHolder = new ValueHolder('a'); + valueHolder.setConvertedValue("this is an a"); + mergedBeanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, + valueHolder); + AutowiredArguments arguments = source.getResolver().resolve(registerBean); + assertThat(arguments.toArray()).hasSize(1); + assertThat(arguments.getObject(0)).isEqualTo("this is an a"); + } + + @Test + void resolveWhenUsingShortcutsInjectsDirectly() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory() { + + @Override + protected Map findAutowireCandidates(String beanName, + Class requiredType, DependencyDescriptor descriptor) { + throw new AssertionError("Should be shortcut"); + } + + }; + AutowiredInstantiationArgumentsResolver resolver = AutowiredInstantiationArgumentsResolver + .forConstructor(String.class); + Source source = new Source(String.class, resolver); + beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = source.registerBean(beanFactory); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> resolver.resolve(registeredBean)); + assertThat(resolver.withShortcuts("one").resolve(registeredBean).toArray()) + .containsExactly("1"); + } + + @Test + void resolveRegistersDependantBeans() { + AutowiredInstantiationArgumentsResolver resolver = AutowiredInstantiationArgumentsResolver + .forConstructor(String.class); + Source source = new Source(SingleArgConstructor.class, resolver); + beanFactory.registerSingleton("one", "1"); + RegisteredBean registeredBean = source.registerBean(this.beanFactory); + resolver.resolve(registeredBean); + assertThat(this.beanFactory.getDependentBeans("one")).containsExactly("testBean"); + } + + /** + * Parameterized {@link Using} test backed by a {@link Sources}. + */ + @Retention(RetentionPolicy.RUNTIME) + @ParameterizedTest + @ArgumentsSource(SourcesArguments.class) + static @interface ParameterizedResolverTest { + + Sources value(); + + } + + /** + * {@link ArgumentsProvider} delegating to the {@link Sources}. + */ + static class SourcesArguments + implements ArgumentsProvider, AnnotationConsumer { + + private Sources source; + + @Override + public void accept(ParameterizedResolverTest annotation) { + this.source = annotation.value(); + } + + @Override + public Stream provideArguments(ExtensionContext context) + throws Exception { + return this.source.provideArguments(context); + } + + } + + /** + * Sources for parameterized tests. + */ + enum Sources { + + SINGLE_ARG { + + @Override + protected void setup() { + add(SingleArgConstructor.class, AutowiredInstantiationArgumentsResolver + .forConstructor(String.class)); + add(String.class, + AutowiredInstantiationArgumentsResolver.forFactoryMethod( + SingleArgFactory.class, "single", String.class)); + } + + }, + + INNER_CLASS_SINGLE_ARG { + + @Override + protected void setup() { + add(Enclosing.InnerSingleArgConstructor.class, + AutowiredInstantiationArgumentsResolver + .forConstructor(String.class)); + add(String.class, + AutowiredInstantiationArgumentsResolver.forFactoryMethod( + Enclosing.InnerSingleArgFactory.class, "single", + String.class)); + } + + }, + + ARRAY_OF_BEANS { + + @Override + protected void setup() { + add(BeansCollectionConstructor.class, + AutowiredInstantiationArgumentsResolver + .forConstructor(String[].class)); + add(String.class, + AutowiredInstantiationArgumentsResolver.forFactoryMethod( + BeansCollectionFactory.class, "array", String[].class)); + } + + }, + + LIST_OF_BEANS { + + @Override + protected void setup() { + add(BeansCollectionConstructor.class, + AutowiredInstantiationArgumentsResolver + .forConstructor(List.class)); + add(String.class, + AutowiredInstantiationArgumentsResolver.forFactoryMethod( + BeansCollectionFactory.class, "list", List.class)); + } + + }, + + SET_OF_BEANS { + + @Override + protected void setup() { + add(BeansCollectionConstructor.class, + AutowiredInstantiationArgumentsResolver + .forConstructor(Set.class)); + add(String.class, + AutowiredInstantiationArgumentsResolver.forFactoryMethod( + BeansCollectionFactory.class, "set", Set.class)); + } + + }, + + MAP_OF_BEANS { + + @Override + protected void setup() { + add(BeansCollectionConstructor.class, + AutowiredInstantiationArgumentsResolver + .forConstructor(Map.class)); + add(String.class, + AutowiredInstantiationArgumentsResolver.forFactoryMethod( + BeansCollectionFactory.class, "map", Map.class)); + } + + }, + + MULTI_ARGS { + + @Override + protected void setup() { + add(MultiArgsConstructor.class, + AutowiredInstantiationArgumentsResolver.forConstructor( + ResourceLoader.class, Environment.class, + ObjectProvider.class)); + add(String.class, + AutowiredInstantiationArgumentsResolver.forFactoryMethod( + MultiArgsFactory.class, "multiArgs", ResourceLoader.class, + Environment.class, ObjectProvider.class)); + } + + }, + + MIXED_ARGS { + + @Override + protected void setup() { + add(MixedArgsConstructor.class, + AutowiredInstantiationArgumentsResolver.forConstructor( + ResourceLoader.class, String.class, Environment.class)); + add(String.class, + AutowiredInstantiationArgumentsResolver.forFactoryMethod( + MixedArgsFactory.class, "mixedArgs", ResourceLoader.class, + String.class, Environment.class)); + } + + }; + + private final List arguments; + + private Sources() { + this.arguments = new ArrayList<>(); + setup(); + } + + protected abstract void setup(); + + protected final void add(Class beanClass, + AutowiredInstantiationArgumentsResolver resolver) { + this.arguments.add(Arguments.of(new Source(beanClass, resolver))); + } + + final Stream provideArguments(ExtensionContext context) { + return this.arguments.stream(); + } + + } + + static class Source { + + private final Class beanClass; + + private final AutowiredInstantiationArgumentsResolver resolver; + + public Source(Class beanClass, + AutowiredInstantiationArgumentsResolver resolver) { + this.beanClass = beanClass; + this.resolver = resolver; + } + + RegisteredBean registerBean(DefaultListableBeanFactory beanFactory) { + return registerBean(beanFactory, beanDefinition -> { + }); + } + + RegisteredBean registerBean(DefaultListableBeanFactory beanFactory, + Consumer beanDefinitionCustomizer) { + String beanName = "testBean"; + RootBeanDefinition beanDefinition = new RootBeanDefinition(this.beanClass); + beanDefinition.setInstanceSupplier(() -> { + throw new BeanCurrentlyInCreationException(beanName); + }); + beanDefinitionCustomizer.accept(beanDefinition); + beanFactory.registerBeanDefinition(beanName, beanDefinition); + return RegisteredBean.of(beanFactory, beanName); + } + + AutowiredInstantiationArgumentsResolver getResolver() { + return this.resolver; + } + + Executable lookupExecutable(RegisteredBean registeredBean) { + return this.resolver.getLookup().get(registeredBean); + } + + @Override + public String toString() { + return this.resolver.getLookup() + " with bean class " + + ClassUtils.getShortName(this.beanClass); + } + + } + + static class NoArgConstructor { + + } + + static class SingleArgConstructor { + + private final String string; + + SingleArgConstructor(String string) { + this.string = string; + } + + String getString() { + return string; + } + + } + + static class SingleArgFactory { + + String single(String s) { + return s; + } + + } + + static class Enclosing { + + class InnerSingleArgConstructor { + + private final String string; + + InnerSingleArgConstructor(String string) { + this.string = string; + } + + String getString() { + return this.string; + } + + } + + class InnerSingleArgFactory { + + String single(String s) { + return s; + } + + } + + } + + static class BeansCollectionConstructor { + + public BeansCollectionConstructor(String[] beans) { + + } + + public BeansCollectionConstructor(List beans) { + + } + + public BeansCollectionConstructor(Set beans) { + + } + + public BeansCollectionConstructor(Map beans) { + + } + + } + + static class BeansCollectionFactory { + + public String array(String[] beans) { + return "test"; + } + + public String list(List beans) { + return "test"; + } + + public String set(Set beans) { + return "test"; + } + + public String map(Map beans) { + return "test"; + } + + } + + static class MultiArgsConstructor { + + public MultiArgsConstructor(ResourceLoader resourceLoader, + Environment environment, ObjectProvider provider) { + } + } + + static class MultiArgsFactory { + + String multiArgs(ResourceLoader resourceLoader, Environment environment, + ObjectProvider provider) { + return "test"; + } + } + + static class MixedArgsConstructor { + + public MixedArgsConstructor(ResourceLoader resourceLoader, String test, + Environment environment) { + + } + + } + + static class MixedArgsFactory { + + String mixedArgs(ResourceLoader resourceLoader, String test, + Environment environment) { + return "test"; + } + + } + + static class CharDependency { + + CharDependency(char escapeChar) { + } + + } + + static interface MethodOnInterface { + + default String test() { + return "Test"; + } + + } + + static class MethodOnInterfaceImpl implements MethodOnInterface { + + } + +} diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredMethodArgumentsResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredMethodArgumentsResolverTests.java new file mode 100644 index 00000000000..5168e8263cd --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredMethodArgumentsResolverTests.java @@ -0,0 +1,228 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.annotation; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.UnsatisfiedDependencyException; +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.env.Environment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link AutowiredMethodArgumentsResolver}. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +class AutowiredMethodArgumentsResolverTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + @Test + void forMethodWhenMethodNameIsEmptyThrowsException() { + String message = "MethodName must not be empty"; + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredMethodArgumentsResolver.forMethod(null)) + .withMessage(message); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredMethodArgumentsResolver.forMethod("")) + .withMessage(message); + assertThatIllegalArgumentException() + .isThrownBy( + () -> AutowiredMethodArgumentsResolver.forRequiredMethod(null)) + .withMessage(message); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredMethodArgumentsResolver.forRequiredMethod(" ")) + .withMessage(message); + } + + @Test + void resolveWhenRegisteredBeanIsNullThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredMethodArgumentsResolver + .forMethod("injectString", String.class).resolve(null)) + .withMessage("RegisteredBean must not be null"); + } + + @Test + void resolveWhenMethodIsMissingThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredMethodArgumentsResolver + .forMethod("missing", InputStream.class).resolve(registeredBean)) + .withMessage( + "Method 'missing' with parameter types [java.io.InputStream] declared on " + + TestBean.class.getName()); + } + + @Test + void resolveRequiredWithSingleDependencyReturnsValue() { + this.beanFactory.registerSingleton("test", "testValue"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredMethodArgumentsResolver resolver = AutowiredMethodArgumentsResolver + .forRequiredMethod("injectString", String.class); + AutowiredArguments resolved = resolver.resolve(registeredBean); + assertThat(resolved.toArray()).containsExactly("testValue"); + } + + @Test + void resolveRequiredWhenNoSuchBeanThrowsUnsatisfiedDependencyException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredMethodArgumentsResolver resolver = AutowiredMethodArgumentsResolver + .forRequiredMethod("injectString", String.class); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) + .isThrownBy(() -> resolver.resolve(registeredBean)).satisfies(ex -> { + assertThat(ex.getBeanName()).isEqualTo("testBean"); + assertThat(ex.getInjectionPoint()).isNotNull(); + assertThat(ex.getInjectionPoint().getMember().getName()) + .isEqualTo("injectString"); + }); + } + + @Test + void resolveNonRequiredWhenNoSuchBeanReturnsNull() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredMethodArgumentsResolver resolver = AutowiredMethodArgumentsResolver + .forMethod("injectString", String.class); + assertThat(resolver.resolve(registeredBean)).isNull(); + } + + @Test + void resolveRequiredWithMultipleDependencesReturnsValue() { + Environment environment = mock(Environment.class); + this.beanFactory.registerSingleton("test", "testValue"); + this.beanFactory.registerSingleton("environment", environment); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredMethodArgumentsResolver resolver = AutowiredMethodArgumentsResolver + .forRequiredMethod("injectStringAndEnvironment", String.class, + Environment.class); + AutowiredArguments resolved = resolver.resolve(registeredBean); + assertThat(resolved.toArray()).containsExactly("testValue", environment); + } + + @Test + void resolveAndInvokeWhenInstanceIsNullThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredMethodArgumentsResolver + .forMethod("injectString", String.class) + .resolveAndInvoke(registeredBean, null)) + .withMessage("Instance must not be null"); + } + + @Test + void resolveAndInvokeInvokesMethod() { + this.beanFactory.registerSingleton("test", "testValue"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredMethodArgumentsResolver resolver = AutowiredMethodArgumentsResolver + .forRequiredMethod("injectString", String.class); + TestBean instance = new TestBean(); + resolver.resolveAndInvoke(registeredBean, instance); + assertThat(instance.getString()).isEqualTo("testValue"); + } + + @Test + void resolveWithActionWhenActionIsNullThrowsException() { + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + assertThatIllegalArgumentException() + .isThrownBy(() -> AutowiredMethodArgumentsResolver + .forMethod("injectString", String.class) + .resolve(registeredBean, null)) + .withMessage("Action must not be null"); + } + + @Test + void resolveWithActionCallsAction() { + this.beanFactory.registerSingleton("test", "testValue"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + List result = new ArrayList<>(); + AutowiredMethodArgumentsResolver.forMethod("injectString", String.class) + .resolve(registeredBean, result::add); + assertThat(result).hasSize(1); + assertThat(((AutowiredArguments) result.get(0)).toArray()) + .containsExactly("testValue"); + } + + @Test + void resolveWhenUsingShortcutsInjectsDirectly() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory() { + + @Override + protected Map findAutowireCandidates(String beanName, + Class requiredType, DependencyDescriptor descriptor) { + throw new AssertionError("Should be shortcut"); + } + + }; + beanFactory.registerSingleton("test", "testValue"); + RegisteredBean registeredBean = registerTestBean(beanFactory); + AutowiredMethodArgumentsResolver resolver = AutowiredMethodArgumentsResolver + .forRequiredMethod("injectString", String.class); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> resolver.resolve(registeredBean)); + assertThat(resolver.withShortcut("test").resolve(registeredBean).getObject(0)) + .isEqualTo("testValue"); + } + + @Test + void resolveRegistersDependantBeans() { + this.beanFactory.registerSingleton("test", "testValue"); + RegisteredBean registeredBean = registerTestBean(this.beanFactory); + AutowiredMethodArgumentsResolver.forMethod("injectString", String.class) + .resolve(registeredBean); + assertThat(this.beanFactory.getDependentBeans("test")) + .containsExactly("testBean"); + } + + private RegisteredBean registerTestBean(DefaultListableBeanFactory beanFactory) { + beanFactory.registerBeanDefinition("testBean", + new RootBeanDefinition(TestBean.class)); + return RegisteredBean.of(beanFactory, "testBean"); + } + + @SuppressWarnings("unused") + static class TestBean { + + private String string; + + void injectString(String string) { + this.string = string; + } + + void injectStringAndEnvironment(String string, Environment environment) { + } + + String getString() { + return this.string; + } + + } + +}