From 2fcae65853fc1620e92493079668c789b433b066 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:17:40 +0100 Subject: [PATCH 1/2] Polishing --- .../annotation/AnnotationBeanNameGeneratorTests.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 884bc07d786..cfbaf501c4b 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 @@ -210,7 +210,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 ""; } @@ -218,7 +218,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 ""; } @@ -260,7 +260,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 ""; } From 17a94fb110659b5986e3fae69479bcad0a47dd71 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 8 Feb 2025 15:31:40 +0100 Subject: [PATCH 2/2] =?UTF-8?q?Improve=20warning=20for=20unexpected=20use?= =?UTF-8?q?=20of=20value=20attribute=20as=20@=E2=81=A0Component=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, if a String 'value' attribute of an annotation was annotated with @⁠AliasFor and explicitly configured to alias an attribute other than @⁠Component.value, the value was still used as the @⁠Component name, but the warning message that was logged stated that the 'value' attribute should be annotated with @⁠AliasFor(annotation=Component.class). However, it is not possible to annotate an annotation attribute twice with @⁠AliasFor. To address that, this commit revises the logic in AnnotationBeanNameGenerator so that it issues a log message similar to the following in such scenarios. WARN o.s.c.a.AnnotationBeanNameGenerator - Although the 'value' attribute in @⁠example.MyStereotype 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. See gh-34346 Closes gh-34317 --- .../AnnotationBeanNameGenerator.java | 41 +++++++--- .../AnnotationBeanNameGeneratorTests.java | 74 ++++++++++++++++++- 2 files changed, 104 insertions(+), 11 deletions(-) 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 3361f0560e5..7828fb03370 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 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. @@ -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; @@ -33,6 +34,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.type.AnnotationMetadata; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -147,16 +150,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 " + @@ -224,7 +237,7 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator { annotationType.equals("jakarta.inject.Named") || annotationType.equals("javax.inject.Named"); - return (isStereotype && attributes.containsKey("value")); + return (isStereotype && attributes.containsKey(MergedAnnotation.VALUE)); } /** @@ -255,4 +268,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 cfbaf501c4b..574038a83b3 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. @@ -168,6 +168,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); @@ -319,7 +338,6 @@ class AnnotationBeanNameGeneratorTests { String[] basePackages() default {}; } - @TestControllerAdvice(basePackages = "com.example", name = "myControllerAdvice") static class ControllerAdviceClass { } @@ -328,4 +346,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 { + } + }