From a68b7289f09c11ece02d665a9874427c5b0d6dc8 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] =?UTF-8?q?Improve=20warning=20for=20unexpected=20use=20of?= =?UTF-8?q?=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 (cherry picked from commit 17a94fb110659b5986e3fae69479bcad0a47dd71) --- .../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 4f0f8a7e62b5..c3f786384fd8 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.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.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 @@ protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotat 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 @@ protected boolean isStereotypeWithNameValue(String annotationType, 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 @@ protected String buildDefaultBeanName(BeanDefinition definition) { 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 cfbaf501c4bc..574038a83b3d 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 @@ void generateBeanNameFromSubStereotypeAnnotationWithStringArrayValueAndExplicitC 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 @@ static class ComposedControllerAnnotationWithStringValue { String[] basePackages() default {}; } - @TestControllerAdvice(basePackages = "com.example", name = "myControllerAdvice") static class ControllerAdviceClass { } @@ -328,4 +346,56 @@ static class ControllerAdviceClass { 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 { + } + }