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; + } + + } + +}