From 14b6339351e8e197083d76089a1f01333c02b5eb Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:38:24 +0100 Subject: [PATCH] =?UTF-8?q?Consistently=20support=20@=E2=81=A0Autowired=20?= =?UTF-8?q?as=20a=20meta-annotation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, the findAutowiredAnnotation() method in AutowiredAnnotationBeanPostProcessor fully supported finding @⁠Autowired as a meta-annotation; however, the isRequired() method in QualifierAnnotationAutowireCandidateResolver only found @⁠Autowired as a "directly present" annotation without any support for meta-annotations. For consistency and to avoid bugs, this commit revises QualifierAnnotationAutowireCandidateResolver so that we always support @⁠Autowired as a meta-annotation. Closes gh-36315 --- ...erAnnotationAutowireCandidateResolver.java | 16 +- ...otationAutowireCandidateResolverTests.java | 179 ++++++++++++++++++ 2 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 spring-beans/src/test/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolverTests.java diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java index 0cd28983347..d18a76ffed3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java @@ -348,8 +348,20 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa if (!super.isRequired(descriptor)) { return false; } - Autowired autowired = descriptor.getAnnotation(Autowired.class); - return (autowired == null || autowired.required()); + + for (Annotation ann : descriptor.getAnnotations()) { + // Directly present? + if (ann instanceof Autowired autowired) { + return autowired.required(); + } + // Meta-present? + Autowired autowired = AnnotationUtils.findAnnotation(ann.annotationType(), Autowired.class); + if (autowired != null) { + return autowired.required(); + } + } + // No @Autowired annotation present: default to true. + return true; } /** diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolverTests.java new file mode 100644 index 00000000000..7d62f92df5f --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolverTests.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-present 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.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.RegisterExtension; + +import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.core.MethodParameter; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +/** + * Unit tests for {@link QualifierAnnotationAutowireCandidateResolver}. + * + * @author Sam Brannen + * @since 7.0.5 + */ +class QualifierAnnotationAutowireCandidateResolverTests { + + final QualifierAnnotationAutowireCandidateResolver resolver = new QualifierAnnotationAutowireCandidateResolver(); + + Method testMethod; + + @RegisterExtension + BeforeTestExecutionCallback extension = context -> this.testMethod = context.getRequiredTestMethod(); + + + @Test + void isNotAutowired() { + assertRequired(); + } + + @Test + void isAutowiredRequired() { + assertRequired(); + } + + @Test + void isAutowiredOptional() { + assertNotRequired(); + } + + @Test + void isMetaAutowiredRequired() { + assertRequired(); + } + + @Test + void isMetaAutowiredOptional() { + assertNotRequired(); + } + + @Test + void isMetaMetaAutowiredRequired() { + assertRequired(); + } + + @Test + void isMetaMetaAutowiredOptional() { + assertNotRequired(); + } + + + private void assertRequired() { + assertSoftly(softly -> { + softly.assertThat(this.resolver.isRequired(getFieldDescriptor())) + .as("%sField is required", this.testMethod.getName()).isTrue(); + softly.assertThat(this.resolver.isRequired(getParameterDescriptor())) + .as("parameter in %sParameter() is required", this.testMethod.getName()).isTrue(); + }); + } + + private void assertNotRequired() { + assertSoftly(softly -> { + softly.assertThat(this.resolver.isRequired(getFieldDescriptor())) + .as("%sField is not required", this.testMethod.getName()).isFalse(); + softly.assertThat(this.resolver.isRequired(getParameterDescriptor())) + .as("parameter in %sParameter() is not required", this.testMethod.getName()).isFalse(); + }); + } + + private DependencyDescriptor getFieldDescriptor() { + var field = ReflectionUtils.findField(getClass(), this.testMethod.getName() + "Field"); + return new DependencyDescriptor(field, true); + } + + private DependencyDescriptor getParameterDescriptor() { + var method = ReflectionUtils.findMethod(getClass(), this.testMethod.getName() + "Parameter", String.class); + var methodParameter = MethodParameter.forExecutable(method, 0); + return new DependencyDescriptor(methodParameter, true); + } + + + String isNotAutowiredField; + + @Autowired + String isAutowiredRequiredField; + + @Autowired(required = false) + String isAutowiredOptionalField; + + @MetaAutowiredRequired + String isMetaAutowiredRequiredField; + + @MetaAutowiredOptional + String isMetaAutowiredOptionalField; + + @MetaMetaAutowiredRequired + String isMetaMetaAutowiredRequiredField; + + @MetaMetaAutowiredOptional + String isMetaMetaAutowiredOptionalField; + + + + void isNotAutowiredParameter(String enigma) { + } + + void isAutowiredRequiredParameter(@Autowired String enigma) { + } + + void isAutowiredOptionalParameter(@Autowired(required = false) String enigma) { + } + + void isMetaAutowiredRequiredParameter(@MetaAutowiredRequired String enigma) { + } + + void isMetaAutowiredOptionalParameter(@MetaAutowiredOptional String enigma) { + } + + void isMetaMetaAutowiredRequiredParameter(@MetaMetaAutowiredRequired String enigma) { + } + + void isMetaMetaAutowiredOptionalParameter(@MetaMetaAutowiredOptional String enigma) { + } + + + @Retention(RetentionPolicy.RUNTIME) + @Autowired + @interface MetaAutowiredRequired { + } + + @Retention(RetentionPolicy.RUNTIME) + @Autowired(required = false) + @interface MetaAutowiredOptional { + } + + @Retention(RetentionPolicy.RUNTIME) + @MetaAutowiredRequired + @interface MetaMetaAutowiredRequired { + } + + @Retention(RetentionPolicy.RUNTIME) + @MetaAutowiredOptional + @interface MetaMetaAutowiredOptional { + } + +}