From 3d7007408961e4229b11f23e85bf13dc8e940774 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:23:41 +0100 Subject: [PATCH] Introduce support for custom parameter names in ParameterResolutionDelegate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The resolveDependency() utility method in ParameterResolutionDelegate resolves a dependency using the name of the parameter as a fallback qualifier. That suffices for most use cases; however, there are times when a custom parameter name should be used instead. For example, for our Bean Override support in the Spring TestContext Framework, an annotation such as @⁠MockitoBean("myBean") specifies an explicit name that should be used instead of name of the annotated parameter. Furthermore, introducing support for custom parameter names will greatly simplify the logic in SpringExtension that will be required to implement #36096. To address those issues, this commit introduces an overloaded variant of resolveDependency() which accepts a custom parameter name. Internally, a custom DependencyDescriptor has been implemented to transparently support this use case. See gh-36096 Closes gh-36534 --- .../ParameterResolutionDelegate.java | 58 +++++++++++++++++-- .../annotation/ParameterResolutionTests.java | 55 ++++++++++++++++++ 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java index 4f4dc58622d..31635d5a5f1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java @@ -89,6 +89,31 @@ public final class ParameterResolutionDelegate { AnnotatedElementUtils.hasAnnotation(annotatedParameter, Value.class)); } + /** + * Resolve the dependency for the supplied {@link Parameter} from the + * supplied {@link AutowireCapableBeanFactory}. + *

See {@link #resolveDependency(Parameter, int, String, Class, AutowireCapableBeanFactory)} + * for details. + * @param parameter the parameter whose dependency should be resolved (must not be + * {@code null}) + * @param parameterIndex the index of the parameter in the constructor or method + * that declares the parameter + * @param containingClass the concrete class that contains the parameter; this may + * differ from the class that declares the parameter in that it may be a subclass + * thereof, potentially substituting type variables (must not be {@code null}) + * @param beanFactory the {@code AutowireCapableBeanFactory} from which to resolve + * the dependency (must not be {@code null}) + * @return the resolved object, or {@code null} if none found + * @throws BeansException if dependency resolution failed + * @see #resolveDependency(Parameter, int, String, Class, AutowireCapableBeanFactory) + */ + public static @Nullable Object resolveDependency( + Parameter parameter, int parameterIndex, Class containingClass, AutowireCapableBeanFactory beanFactory) + throws BeansException { + + return resolveDependency(parameter, parameterIndex, null, containingClass, beanFactory); + } + /** * Resolve the dependency for the supplied {@link Parameter} from the * supplied {@link AutowireCapableBeanFactory}. @@ -101,11 +126,13 @@ public final class ParameterResolutionDelegate { * with {@link Autowired @Autowired} with the {@link Autowired#required required} * flag set to {@code false}. *

If an explicit qualifier is not declared, the name of the parameter - * will be used as the qualifier for resolving ambiguities. + * (or a supplied custom name) will be used as the qualifier for resolving ambiguities. * @param parameter the parameter whose dependency should be resolved (must not be * {@code null}) * @param parameterIndex the index of the parameter in the constructor or method * that declares the parameter + * @param parameterName a custom name for the parameter; or {@code null} to use + * the default parameter name discovery logic * @param containingClass the concrete class that contains the parameter; this may * differ from the class that declares the parameter in that it may be a subclass * thereof, potentially substituting type variables (must not be {@code null}) @@ -113,13 +140,14 @@ public final class ParameterResolutionDelegate { * the dependency (must not be {@code null}) * @return the resolved object, or {@code null} if none found * @throws BeansException if dependency resolution failed + * @since 7.1 * @see #isAutowirable * @see Autowired#required * @see SynthesizingMethodParameter#forExecutable(Executable, int) * @see AutowireCapableBeanFactory#resolveDependency(DependencyDescriptor, String) */ - public static @Nullable Object resolveDependency( - Parameter parameter, int parameterIndex, Class containingClass, AutowireCapableBeanFactory beanFactory) + public static @Nullable Object resolveDependency(Parameter parameter, int parameterIndex, + @Nullable String parameterName, Class containingClass, AutowireCapableBeanFactory beanFactory) throws BeansException { Assert.notNull(parameter, "Parameter must not be null"); @@ -132,7 +160,7 @@ public final class ParameterResolutionDelegate { MethodParameter methodParameter = SynthesizingMethodParameter.forExecutable( parameter.getDeclaringExecutable(), parameterIndex); - DependencyDescriptor descriptor = new DependencyDescriptor(methodParameter, required); + DependencyDescriptor descriptor = new NamedParameterDependencyDescriptor(methodParameter, required, parameterName); descriptor.setContainingClass(containingClass); return beanFactory.resolveDependency(descriptor, null); } @@ -171,4 +199,26 @@ public final class ParameterResolutionDelegate { return parameter; } + + @SuppressWarnings("serial") + private static class NamedParameterDependencyDescriptor extends DependencyDescriptor { + + private final @Nullable String parameterName; + + NamedParameterDependencyDescriptor(MethodParameter methodParameter, boolean required, @Nullable String parameterName) { + super(methodParameter, required); + this.parameterName = parameterName; + } + + @Override + public @Nullable String getDependencyName() { + return (this.parameterName != null ? this.parameterName : super.getDependencyName()); + } + + @Override + public boolean usesStandardBeanLookup() { + return true; + } + } + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java index 3ec16e2fef1..08ccff1b759 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java @@ -134,9 +134,64 @@ class ParameterResolutionTests { parameter, parameterIndex, AutowirableClass.class, beanFactory); assertThat(intermediateDependencyDescriptor.getAnnotatedElement()).isEqualTo(constructor); assertThat(intermediateDependencyDescriptor.getMethodParameter().getParameter()).isEqualTo(parameter); + assertThat(intermediateDependencyDescriptor.usesStandardBeanLookup()).isTrue(); } } + @Test + void resolveDependencyWithCustomParameterNamePreconditionsForParameter() { + assertThatIllegalArgumentException() + .isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(null, 0, "customName", getClass(), mock())) + .withMessageContaining("Parameter must not be null"); + } + + @Test + void resolveDependencyWithCustomParameterNamePreconditionsForContainingClass() { + assertThatIllegalArgumentException() + .isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, "customName", null, mock())) + .withMessageContaining("Containing class must not be null"); + } + + @Test + void resolveDependencyWithCustomParameterNamePreconditionsForBeanFactory() { + assertThatIllegalArgumentException() + .isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, "customName", getClass(), null)) + .withMessageContaining("AutowireCapableBeanFactory must not be null"); + } + + @Test + void resolveDependencyWithNullCustomParameterNameFallsBackToDefaultParameterNameDiscovery() throws Exception { + Constructor constructor = AutowirableClass.class.getConstructor(String.class, String.class, String.class, String.class); + AutowireCapableBeanFactory beanFactory = mock(); + given(beanFactory.resolveDependency(any(), isNull())).willAnswer(invocation -> invocation.getArgument(0)); + + Parameter[] parameters = constructor.getParameters(); + for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) { + Parameter parameter = parameters[parameterIndex]; + DependencyDescriptor via4ArgMethod = (DependencyDescriptor) ParameterResolutionDelegate.resolveDependency( + parameter, parameterIndex, AutowirableClass.class, beanFactory); + DependencyDescriptor via5ArgMethod = (DependencyDescriptor) ParameterResolutionDelegate.resolveDependency( + parameter, parameterIndex, null, AutowirableClass.class, beanFactory); + assertThat(via5ArgMethod.getDependencyName()).isEqualTo(via4ArgMethod.getDependencyName()); + } + } + + @Test + void resolveDependencyWithCustomParameterName() throws Exception { + Constructor constructor = AutowirableClass.class.getConstructor(String.class, String.class, String.class, String.class); + AutowireCapableBeanFactory beanFactory = mock(); + given(beanFactory.resolveDependency(any(), isNull())).willAnswer(invocation -> invocation.getArgument(0)); + + Parameter parameter = constructor.getParameters()[0]; + DependencyDescriptor descriptor = (DependencyDescriptor) ParameterResolutionDelegate.resolveDependency( + parameter, 0, "customBeanName", AutowirableClass.class, beanFactory); + + assertThat(descriptor.getAnnotatedElement()).isEqualTo(constructor); + assertThat(descriptor.getMethodParameter().getParameter()).isEqualTo(parameter); + assertThat(descriptor.getDependencyName()).isEqualTo("customBeanName"); + assertThat(descriptor.usesStandardBeanLookup()).isTrue(); + } + void autowirableMethod( @Autowired String firstParameter,