diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java index 8529a4cdf1b..5c834b1e010 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java @@ -17,6 +17,7 @@ package org.springframework.context.annotation; import java.lang.annotation.Annotation; +import java.lang.reflect.Method; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; @@ -34,6 +35,7 @@ import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation.Adapt; @@ -41,6 +43,7 @@ import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.type.AnnotationMetadata; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -143,16 +146,26 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator { Set metaAnnotationTypes = this.metaAnnotationTypesCache.computeIfAbsent(annotationType, key -> getMetaAnnotationTypes(mergedAnnotation)); if (isStereotypeWithNameValue(annotationType, metaAnnotationTypes, attributes)) { - Object value = attributes.get("value"); + Object value = attributes.get(MergedAnnotation.VALUE); if (value instanceof String currentName && !currentName.isBlank()) { if (conventionBasedStereotypeCheckCache.add(annotationType) && metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) && logger.isWarnEnabled()) { - logger.warn(""" - Support for convention-based stereotype names is deprecated and will \ - be removed in a future version of the framework. Please annotate the \ - 'value' attribute in @%s with @AliasFor(annotation=Component.class) \ - to declare an explicit alias for @Component's 'value' attribute.""" - .formatted(annotationType)); + if (hasExplicitlyAliasedValueAttribute(mergedAnnotation.getType())) { + logger.warn(""" + Although the 'value' attribute in @%s declares @AliasFor for an attribute \ + other than @Component's 'value' attribute, the value is still used as the \ + @Component name based on convention. As of Spring Framework 7.0, such a \ + 'value' attribute will no longer be used as the @Component name.""" + .formatted(annotationType)); + } + else { + logger.warn(""" + Support for convention-based @Component names is deprecated and will \ + be removed in a future version of the framework. Please annotate the \ + 'value' attribute in @%s with @AliasFor(annotation=Component.class) \ + to declare an explicit alias for @Component's 'value' attribute.""" + .formatted(annotationType)); + } } if (beanName != null && !currentName.equals(beanName)) { throw new IllegalStateException("Stereotype annotations suggest inconsistent " + @@ -216,7 +229,7 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator { boolean isStereotype = metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) || annotationType.equals("jakarta.inject.Named"); - return (isStereotype && attributes.containsKey("value")); + return (isStereotype && attributes.containsKey(MergedAnnotation.VALUE)); } /** @@ -247,4 +260,14 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator { return StringUtils.uncapitalizeAsProperty(shortClassName); } + /** + * Determine if the supplied annotation type declares a {@code value()} attribute + * with an explicit alias configured via {@link AliasFor @AliasFor}. + * @since 6.2.3 + */ + private static boolean hasExplicitlyAliasedValueAttribute(Class annotationType) { + Method valueAttribute = ReflectionUtils.findMethod(annotationType, MergedAnnotation.VALUE); + return (valueAttribute != null && valueAttribute.isAnnotationPresent(AliasFor.class)); + } + } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java index 8af7c9a322c..e8174741824 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -150,6 +150,25 @@ class AnnotationBeanNameGeneratorTests { assertGeneratedName(RestControllerAdviceClass.class, "myRestControllerAdvice"); } + @Test // gh-34317 + void generateBeanNameFromStereotypeAnnotationWithStringValueAsExplicitAliasForMetaAnnotationOtherThanComponent() { + // As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name. + // As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithoutExplicitName". + assertGeneratedName(StereotypeWithoutExplicitName.class, "enigma"); + } + + @Test // gh-34317 + void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentNameWithBlankName() { + // As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name. + // As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithGeneratedName". + assertGeneratedName(StereotypeWithGeneratedName.class, "enigma"); + } + + @Test // gh-34317 + void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentName() { + assertGeneratedName(StereotypeWithExplicitName.class, "explicitName"); + } + private void assertGeneratedName(Class clazz, String expectedName) { BeanDefinition bd = annotatedBeanDef(clazz); @@ -192,7 +211,7 @@ class AnnotationBeanNameGeneratorTests { @Retention(RetentionPolicy.RUNTIME) @Component @interface ConventionBasedComponent1 { - // This intentionally convention-based. Please do not add @AliasFor. + // This is intentionally convention-based. Please do not add @AliasFor. // See gh-31093. String value() default ""; } @@ -200,7 +219,7 @@ class AnnotationBeanNameGeneratorTests { @Retention(RetentionPolicy.RUNTIME) @Component @interface ConventionBasedComponent2 { - // This intentionally convention-based. Please do not add @AliasFor. + // This is intentionally convention-based. Please do not add @AliasFor. // See gh-31093. String value() default ""; } @@ -242,7 +261,7 @@ class AnnotationBeanNameGeneratorTests { @Target(ElementType.TYPE) @Controller @interface TestRestController { - // This intentionally convention-based. Please do not add @AliasFor. + // This is intentionally convention-based. Please do not add @AliasFor. // See gh-31093. String value() default ""; } @@ -301,7 +320,6 @@ class AnnotationBeanNameGeneratorTests { String[] basePackages() default {}; } - @TestControllerAdvice(basePackages = "com.example", name = "myControllerAdvice") static class ControllerAdviceClass { } @@ -310,4 +328,56 @@ class AnnotationBeanNameGeneratorTests { static class RestControllerAdviceClass { } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.ANNOTATION_TYPE) + @interface MetaAnnotationWithStringAttribute { + + String attribute() default ""; + } + + /** + * Custom stereotype annotation which has a {@code String value} attribute that + * is explicitly declared as an alias for an attribute in a meta-annotation + * other than {@link Component @Component}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Component + @MetaAnnotationWithStringAttribute + @interface MyStereotype { + + @AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute") + String value() default ""; + } + + @MyStereotype("enigma") + static class StereotypeWithoutExplicitName { + } + + /** + * Custom stereotype annotation which is identical to {@link MyStereotype @MyStereotype} + * except that it has a {@link #name} attribute that is an explicit alias for + * {@link Component#value}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Component + @MetaAnnotationWithStringAttribute + @interface MyNamedStereotype { + + @AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute") + String value() default ""; + + @AliasFor(annotation = Component.class, attribute = "value") + String name() default ""; + } + + @MyNamedStereotype(value = "enigma", name ="explicitName") + static class StereotypeWithExplicitName { + } + + @MyNamedStereotype(value = "enigma") + static class StereotypeWithGeneratedName { + } + }