Skip to content

Commit a68b728

Browse files
committed
Improve warning for unexpected use of value attribute as @⁠Component name
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 17a94fb)
1 parent a5b9e66 commit a68b728

File tree

2 files changed

+104
-11
lines changed

2 files changed

+104
-11
lines changed

spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java

+32-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.context.annotation;
1818

1919
import java.lang.annotation.Annotation;
20+
import java.lang.reflect.Method;
2021
import java.util.Collections;
2122
import java.util.HashSet;
2223
import java.util.LinkedHashSet;
@@ -33,6 +34,7 @@
3334
import org.springframework.beans.factory.config.BeanDefinition;
3435
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
3536
import org.springframework.beans.factory.support.BeanNameGenerator;
37+
import org.springframework.core.annotation.AliasFor;
3638
import org.springframework.core.annotation.AnnotationAttributes;
3739
import org.springframework.core.annotation.MergedAnnotation;
3840
import org.springframework.core.annotation.MergedAnnotation.Adapt;
@@ -41,6 +43,7 @@
4143
import org.springframework.lang.Nullable;
4244
import org.springframework.util.Assert;
4345
import org.springframework.util.ClassUtils;
46+
import org.springframework.util.ReflectionUtils;
4447
import org.springframework.util.StringUtils;
4548

4649
/**
@@ -147,16 +150,26 @@ protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotat
147150
Set<String> metaAnnotationTypes = this.metaAnnotationTypesCache.computeIfAbsent(annotationType,
148151
key -> getMetaAnnotationTypes(mergedAnnotation));
149152
if (isStereotypeWithNameValue(annotationType, metaAnnotationTypes, attributes)) {
150-
Object value = attributes.get("value");
153+
Object value = attributes.get(MergedAnnotation.VALUE);
151154
if (value instanceof String currentName && !currentName.isBlank()) {
152155
if (conventionBasedStereotypeCheckCache.add(annotationType) &&
153156
metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) && logger.isWarnEnabled()) {
154-
logger.warn("""
155-
Support for convention-based stereotype names is deprecated and will \
156-
be removed in a future version of the framework. Please annotate the \
157-
'value' attribute in @%s with @AliasFor(annotation=Component.class) \
158-
to declare an explicit alias for @Component's 'value' attribute."""
159-
.formatted(annotationType));
157+
if (hasExplicitlyAliasedValueAttribute(mergedAnnotation.getType())) {
158+
logger.warn("""
159+
Although the 'value' attribute in @%s declares @AliasFor for an attribute \
160+
other than @Component's 'value' attribute, the value is still used as the \
161+
@Component name based on convention. As of Spring Framework 7.0, such a \
162+
'value' attribute will no longer be used as the @Component name."""
163+
.formatted(annotationType));
164+
}
165+
else {
166+
logger.warn("""
167+
Support for convention-based @Component names is deprecated and will \
168+
be removed in a future version of the framework. Please annotate the \
169+
'value' attribute in @%s with @AliasFor(annotation=Component.class) \
170+
to declare an explicit alias for @Component's 'value' attribute."""
171+
.formatted(annotationType));
172+
}
160173
}
161174
if (beanName != null && !currentName.equals(beanName)) {
162175
throw new IllegalStateException("Stereotype annotations suggest inconsistent " +
@@ -224,7 +237,7 @@ protected boolean isStereotypeWithNameValue(String annotationType,
224237
annotationType.equals("jakarta.inject.Named") ||
225238
annotationType.equals("javax.inject.Named");
226239

227-
return (isStereotype && attributes.containsKey("value"));
240+
return (isStereotype && attributes.containsKey(MergedAnnotation.VALUE));
228241
}
229242

230243
/**
@@ -255,4 +268,14 @@ protected String buildDefaultBeanName(BeanDefinition definition) {
255268
return StringUtils.uncapitalizeAsProperty(shortClassName);
256269
}
257270

271+
/**
272+
* Determine if the supplied annotation type declares a {@code value()} attribute
273+
* with an explicit alias configured via {@link AliasFor @AliasFor}.
274+
* @since 6.2.3
275+
*/
276+
private static boolean hasExplicitlyAliasedValueAttribute(Class<? extends Annotation> annotationType) {
277+
Method valueAttribute = ReflectionUtils.findMethod(annotationType, MergedAnnotation.VALUE);
278+
return (valueAttribute != null && valueAttribute.isAnnotationPresent(AliasFor.class));
279+
}
280+
258281
}

spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java

+72-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -168,6 +168,25 @@ void generateBeanNameFromSubStereotypeAnnotationWithStringArrayValueAndExplicitC
168168
assertGeneratedName(RestControllerAdviceClass.class, "myRestControllerAdvice");
169169
}
170170

171+
@Test // gh-34317
172+
void generateBeanNameFromStereotypeAnnotationWithStringValueAsExplicitAliasForMetaAnnotationOtherThanComponent() {
173+
// As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name.
174+
// As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithoutExplicitName".
175+
assertGeneratedName(StereotypeWithoutExplicitName.class, "enigma");
176+
}
177+
178+
@Test // gh-34317
179+
void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentNameWithBlankName() {
180+
// As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name.
181+
// As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithGeneratedName".
182+
assertGeneratedName(StereotypeWithGeneratedName.class, "enigma");
183+
}
184+
185+
@Test // gh-34317
186+
void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentName() {
187+
assertGeneratedName(StereotypeWithExplicitName.class, "explicitName");
188+
}
189+
171190

172191
private void assertGeneratedName(Class<?> clazz, String expectedName) {
173192
BeanDefinition bd = annotatedBeanDef(clazz);
@@ -319,7 +338,6 @@ static class ComposedControllerAnnotationWithStringValue {
319338
String[] basePackages() default {};
320339
}
321340

322-
323341
@TestControllerAdvice(basePackages = "com.example", name = "myControllerAdvice")
324342
static class ControllerAdviceClass {
325343
}
@@ -328,4 +346,56 @@ static class ControllerAdviceClass {
328346
static class RestControllerAdviceClass {
329347
}
330348

349+
@Retention(RetentionPolicy.RUNTIME)
350+
@Target(ElementType.ANNOTATION_TYPE)
351+
@interface MetaAnnotationWithStringAttribute {
352+
353+
String attribute() default "";
354+
}
355+
356+
/**
357+
* Custom stereotype annotation which has a {@code String value} attribute that
358+
* is explicitly declared as an alias for an attribute in a meta-annotation
359+
* other than {@link Component @Component}.
360+
*/
361+
@Retention(RetentionPolicy.RUNTIME)
362+
@Target(ElementType.TYPE)
363+
@Component
364+
@MetaAnnotationWithStringAttribute
365+
@interface MyStereotype {
366+
367+
@AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute")
368+
String value() default "";
369+
}
370+
371+
@MyStereotype("enigma")
372+
static class StereotypeWithoutExplicitName {
373+
}
374+
375+
/**
376+
* Custom stereotype annotation which is identical to {@link MyStereotype @MyStereotype}
377+
* except that it has a {@link #name} attribute that is an explicit alias for
378+
* {@link Component#value}.
379+
*/
380+
@Retention(RetentionPolicy.RUNTIME)
381+
@Target(ElementType.TYPE)
382+
@Component
383+
@MetaAnnotationWithStringAttribute
384+
@interface MyNamedStereotype {
385+
386+
@AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute")
387+
String value() default "";
388+
389+
@AliasFor(annotation = Component.class, attribute = "value")
390+
String name() default "";
391+
}
392+
393+
@MyNamedStereotype(value = "enigma", name ="explicitName")
394+
static class StereotypeWithExplicitName {
395+
}
396+
397+
@MyNamedStereotype(value = "enigma")
398+
static class StereotypeWithGeneratedName {
399+
}
400+
331401
}

0 commit comments

Comments
 (0)