Skip to content

Commit ff104b6

Browse files
committed
Look up @component stereotype names using @AliasFor semantics
Although gh-20615 introduced the use of @AliasFor for @component(value) in the built-in stereotype annotations (@service, @controller, @repository, @configuration, and @RestController), prior to this commit the framework did not actually rely on @AliasFor support when looking up a component name via stereotype annotations. Rather, the framework had custom annotation parsing logic in AnnotationBeanNameGenerator#determineBeanNameFromAnnotation() which effectively ignored explicit annotation attribute overrides configured via @AliasFor. This commit revises AnnotationBeanNameGenerator#determineBeanNameFromAnnotation() so that it first looks up @component stereotype names using @AliasFor semantics before falling back to the "convention-based" component name lookup strategy. Consequently, the name of the annotation attribute that is used to specify the bean name is no longer required to be `value`, and custom stereotype annotations can now declare an attribute with a different name (such as `name`) and annotate that attribute with `@AliasFor(annotation = Component.class, attribute = "value")`. Closes gh-31089
1 parent d189e16 commit ff104b6

File tree

8 files changed

+171
-26
lines changed

8 files changed

+171
-26
lines changed

framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc

+12-3
Original file line numberDiff line numberDiff line change
@@ -665,9 +665,18 @@ analogous to how the container selects between multiple `@Autowired` constructor
665665

666666
When a component is autodetected as part of the scanning process, its bean name is
667667
generated by the `BeanNameGenerator` strategy known to that scanner. By default, any
668-
Spring stereotype annotation (`@Component`, `@Repository`, `@Service`, and
669-
`@Controller`) that contains a name `value` thereby provides that name to the
670-
corresponding bean definition.
668+
Spring stereotype annotation (`@Component`, `@Repository`, `@Service`, `@Controller`,
669+
`@Configuration`, and so forth) that contains a non-empty `value` attribute provides that
670+
value as the name to the corresponding bean definition.
671+
672+
[NOTE]
673+
====
674+
As of Spring Framework 6.1, the name of the annotation attribute that is used to specify
675+
the bean name is no longer required to be `value`. Custom stereotype annotations can
676+
declare an attribute with a different name (such as `name`) and annotate that attribute
677+
with `@AliasFor(annotation = Component.class, attribute = "value")`. See the source code
678+
declaration of `Repository#value()` for a concrete example.
679+
====
671680

672681
If such an annotation contains no name `value` or for any other detected component
673682
(such as those discovered by custom filters), the default bean name generator returns

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

+39-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.context.annotation;
1818

1919
import java.util.Collections;
20+
import java.util.List;
2021
import java.util.Map;
2122
import java.util.Set;
2223
import java.util.concurrent.ConcurrentHashMap;
@@ -26,6 +27,7 @@
2627
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
2728
import org.springframework.beans.factory.support.BeanNameGenerator;
2829
import org.springframework.core.annotation.AnnotationAttributes;
30+
import org.springframework.core.annotation.MergedAnnotation;
2931
import org.springframework.core.type.AnnotationMetadata;
3032
import org.springframework.lang.Nullable;
3133
import org.springframework.util.Assert;
@@ -100,6 +102,12 @@ public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry
100102
protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) {
101103
AnnotationMetadata metadata = annotatedDef.getMetadata();
102104
Set<String> annotationTypes = metadata.getAnnotationTypes();
105+
106+
String explicitBeanName = getExplicitBeanName(metadata);
107+
if (explicitBeanName != null) {
108+
return explicitBeanName;
109+
}
110+
103111
String beanName = null;
104112
for (String annotationType : annotationTypes) {
105113
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, annotationType);
@@ -123,6 +131,36 @@ protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotat
123131
return beanName;
124132
}
125133

134+
/**
135+
* Get the explicit bean name for the underlying class, as configured via
136+
* {@link org.springframework.stereotype.Component @Component} and taking into
137+
* account {@link org.springframework.core.annotation.AliasFor @AliasFor}
138+
* semantics for annotation attribute overrides for {@code @Component}'s
139+
* {@code value} attribute.
140+
* @param metadata the {@link AnnotationMetadata} for the underlying class
141+
* @return the explicit bean name, or {@code null} if not found
142+
* @since 6.1
143+
* @see org.springframework.stereotype.Component#value()
144+
*/
145+
@Nullable
146+
private String getExplicitBeanName(AnnotationMetadata metadata) {
147+
List<String> names = metadata.getAnnotations().stream(COMPONENT_ANNOTATION_CLASSNAME)
148+
.map(annotation -> annotation.getString(MergedAnnotation.VALUE))
149+
.filter(StringUtils::hasText)
150+
.map(String::trim)
151+
.distinct()
152+
.toList();
153+
154+
if (names.size() == 1) {
155+
return names.get(0);
156+
}
157+
if (names.size() > 1) {
158+
throw new IllegalStateException(
159+
"Stereotype annotations suggest inconsistent component names: " + names);
160+
}
161+
return null;
162+
}
163+
126164
/**
127165
* Check whether the given annotation is a stereotype that is allowed
128166
* to suggest a component name through its {@code value()} attribute.
@@ -134,8 +172,7 @@ protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotat
134172
protected boolean isStereotypeWithNameValue(String annotationType,
135173
Set<String> metaAnnotationTypes, @Nullable Map<String, Object> attributes) {
136174

137-
boolean isStereotype = annotationType.equals(COMPONENT_ANNOTATION_CLASSNAME) ||
138-
metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) ||
175+
boolean isStereotype = metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) ||
139176
annotationType.equals("jakarta.annotation.ManagedBean") ||
140177
annotationType.equals("javax.annotation.ManagedBean") ||
141178
annotationType.equals("jakarta.inject.Named") ||

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

+1
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@
434434
* {@link AnnotationConfigApplicationContext}. If the {@code @Configuration} class
435435
* is registered as a traditional XML bean definition, the name/id of the bean
436436
* element will take precedence.
437+
* <p>Alias for {@link Component#value}.
437438
* @return the explicit component name, if any (or empty String otherwise)
438439
* @see AnnotationBeanNameGenerator
439440
*/

spring-context/src/main/java/org/springframework/stereotype/Component.java

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -28,11 +28,17 @@
2828
* when using annotation-based configuration and classpath scanning.
2929
*
3030
* <p>Other class-level annotations may be considered as identifying
31-
* a component as well, typically a special kind of component:
32-
* e.g. the {@link Repository @Repository} annotation or AspectJ's
31+
* a component as well, typically a special kind of component &mdash;
32+
* for example, the {@link Repository @Repository} annotation or AspectJ's
3333
* {@link org.aspectj.lang.annotation.Aspect @Aspect} annotation.
3434
*
35+
* <p>As of Spring Framework 6.1, custom component stereotype annotations should
36+
* use {@link org.springframework.core.annotation.AliasFor @AliasFor} to declare
37+
* an explicit alias for this annotation's {@link #value} attribute. See the
38+
* source code declaration of {@link Repository#value()} for a concrete example.
39+
*
3540
* @author Mark Fisher
41+
* @author Sam Brannen
3642
* @since 2.5
3743
* @see Repository
3844
* @see Service
@@ -47,7 +53,7 @@
4753

4854
/**
4955
* The value may indicate a suggestion for a logical component name,
50-
* to be turned into a Spring bean in case of an autodetected component.
56+
* to be turned into a Spring bean name in case of an autodetected component.
5157
* @return the suggested component name, if any (or empty String otherwise)
5258
*/
5359
String value() default "";

spring-context/src/main/java/org/springframework/stereotype/Controller.java

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -46,9 +46,7 @@
4646
public @interface Controller {
4747

4848
/**
49-
* The value may indicate a suggestion for a logical component name,
50-
* to be turned into a Spring bean in case of an autodetected component.
51-
* @return the suggested component name, if any (or empty String otherwise)
49+
* Alias for {@link Component#value}.
5250
*/
5351
@AliasFor(annotation = Component.class)
5452
String value() default "";

spring-context/src/main/java/org/springframework/stereotype/Repository.java

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -43,9 +43,8 @@
4343
* to its role in the overall application architecture for the purpose of tooling,
4444
* aspects, etc.
4545
*
46-
* <p>As of Spring 2.5, this annotation also serves as a specialization of
47-
* {@link Component @Component}, allowing for implementation classes to be autodetected
48-
* through classpath scanning.
46+
* <p>This annotation also serves as a specialization of {@link Component @Component},
47+
* allowing for implementation classes to be autodetected through classpath scanning.
4948
*
5049
* @author Rod Johnson
5150
* @author Juergen Hoeller
@@ -62,9 +61,7 @@
6261
public @interface Repository {
6362

6463
/**
65-
* The value may indicate a suggestion for a logical component name,
66-
* to be turned into a Spring bean in case of an autodetected component.
67-
* @return the suggested component name, if any (or empty String otherwise)
64+
* Alias for {@link Component#value}.
6865
*/
6966
@AliasFor(annotation = Component.class)
7067
String value() default "";

spring-context/src/main/java/org/springframework/stereotype/Service.java

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -48,9 +48,7 @@
4848
public @interface Service {
4949

5050
/**
51-
* The value may indicate a suggestion for a logical component name,
52-
* to be turned into a Spring bean in case of an autodetected component.
53-
* @return the suggested component name, if any (or empty String otherwise)
51+
* Alias for {@link Component#value}.
5452
*/
5553
@AliasFor(annotation = Component.class)
5654
String value() default "";

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

+101-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.lang.annotation.Retention;
2121
import java.lang.annotation.RetentionPolicy;
2222
import java.lang.annotation.Target;
23+
import java.util.List;
2324

2425
import example.scannable.DefaultNamedComponent;
2526
import example.scannable.JakartaManagedBeanComponent;
@@ -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.SimpleBeanDefinitionRegistry;
37+
import org.springframework.core.annotation.AliasFor;
3638
import org.springframework.stereotype.Component;
3739
import org.springframework.stereotype.Controller;
3840
import org.springframework.stereotype.Service;
@@ -73,20 +75,34 @@ void generateBeanNameWithNamedComponentWhereTheNameIsBlank() {
7375
assertGeneratedNameIsDefault(ComponentWithBlankName.class);
7476
}
7577

78+
@Test
79+
void generateBeanNameForConventionBasedComponentWithDuplicateIdenticalNames() {
80+
assertGeneratedName(ConventionBasedComponentWithDuplicateIdenticalNames.class, "myComponent");
81+
}
82+
7683
@Test
7784
void generateBeanNameForComponentWithDuplicateIdenticalNames() {
7885
assertGeneratedName(ComponentWithDuplicateIdenticalNames.class, "myComponent");
7986
}
8087

8188
@Test
82-
void generateBeanNameForComponentWithConflictingNames() {
83-
BeanDefinition bd = annotatedBeanDef(ComponentWithMultipleConflictingNames.class);
89+
void generateBeanNameForConventionBasedComponentWithConflictingNames() {
90+
BeanDefinition bd = annotatedBeanDef(ConventionBasedComponentWithMultipleConflictingNames.class);
8491
assertThatIllegalStateException()
8592
.isThrownBy(() -> generateBeanName(bd))
8693
.withMessage("Stereotype annotations suggest inconsistent component names: '%s' versus '%s'",
8794
"myComponent", "myService");
8895
}
8996

97+
@Test
98+
void generateBeanNameForComponentWithConflictingNames() {
99+
BeanDefinition bd = annotatedBeanDef(ComponentWithMultipleConflictingNames.class);
100+
assertThatIllegalStateException()
101+
.isThrownBy(() -> generateBeanName(bd))
102+
.withMessage("Stereotype annotations suggest inconsistent component names: " +
103+
List.of("myComponent", "myService"));
104+
}
105+
90106
@Test
91107
void generateBeanNameWithJakartaNamedComponent() {
92108
assertGeneratedName(JakartaNamedComponent.class, "myJakartaNamedComponent");
@@ -142,6 +158,16 @@ void generateBeanNameFromComposedControllerAnnotationWithStringValue() {
142158
assertGeneratedName(ComposedControllerAnnotationWithStringValue.class, "restController");
143159
}
144160

161+
@Test // gh-31089
162+
void generateBeanNameFromStereotypeAnnotationWithStringArrayValueAndExplicitComponentNameAlias() {
163+
assertGeneratedName(ControllerAdviceClass.class, "myControllerAdvice");
164+
}
165+
166+
@Test // gh-31089
167+
void generateBeanNameFromSubStereotypeAnnotationWithStringArrayValueAndExplicitComponentNameAlias() {
168+
assertGeneratedName(RestControllerAdviceClass.class, "myRestControllerAdvice");
169+
}
170+
145171

146172
private void assertGeneratedName(Class<?> clazz, String expectedName) {
147173
BeanDefinition bd = annotatedBeanDef(clazz);
@@ -181,6 +207,28 @@ static class ComponentWithDuplicateIdenticalNames {
181207
static class ComponentWithMultipleConflictingNames {
182208
}
183209

210+
@Retention(RetentionPolicy.RUNTIME)
211+
@Component
212+
@interface ConventionBasedComponent1 {
213+
String value() default "";
214+
}
215+
216+
@Retention(RetentionPolicy.RUNTIME)
217+
@Component
218+
@interface ConventionBasedComponent2 {
219+
String value() default "";
220+
}
221+
222+
@ConventionBasedComponent1("myComponent")
223+
@ConventionBasedComponent2("myComponent")
224+
static class ConventionBasedComponentWithDuplicateIdenticalNames {
225+
}
226+
227+
@ConventionBasedComponent1("myComponent")
228+
@ConventionBasedComponent2("myService")
229+
static class ConventionBasedComponentWithMultipleConflictingNames {
230+
}
231+
184232
@Component
185233
private static class AnonymousComponent {
186234
}
@@ -224,4 +272,55 @@ static class ComposedControllerAnnotationWithBlankName {
224272
static class ComposedControllerAnnotationWithStringValue {
225273
}
226274

275+
/**
276+
* Mock of {@code org.springframework.web.bind.annotation.ControllerAdvice},
277+
* which also has a {@code value} attribute that is NOT a {@code String} that
278+
* is meant to be used for the component name.
279+
* <p>Declares a custom {@link #name} that explicitly aliases {@link Component#value()}.
280+
*/
281+
@Retention(RetentionPolicy.RUNTIME)
282+
@Target(ElementType.TYPE)
283+
@Component
284+
@interface TestControllerAdvice {
285+
286+
@AliasFor(annotation = Component.class, attribute = "value")
287+
String name() default "";
288+
289+
@AliasFor("basePackages")
290+
String[] value() default {};
291+
292+
@AliasFor("value")
293+
String[] basePackages() default {};
294+
}
295+
296+
/**
297+
* Mock of {@code org.springframework.web.bind.annotation.RestControllerAdvice},
298+
* which also has a {@code value} attribute that is NOT a {@code String} that
299+
* is meant to be used for the component name.
300+
* <p>Declares a custom {@link #name} that explicitly aliases
301+
* {@link TestControllerAdvice#name()} instead of {@link Component#value()}.
302+
*/
303+
@Retention(RetentionPolicy.RUNTIME)
304+
@TestControllerAdvice
305+
@interface TestRestControllerAdvice {
306+
307+
@AliasFor(annotation = TestControllerAdvice.class)
308+
String name() default "";
309+
310+
@AliasFor(annotation = TestControllerAdvice.class)
311+
String[] value() default {};
312+
313+
@AliasFor(annotation = TestControllerAdvice.class)
314+
String[] basePackages() default {};
315+
}
316+
317+
318+
@TestControllerAdvice(basePackages = "com.example", name = "myControllerAdvice")
319+
static class ControllerAdviceClass {
320+
}
321+
322+
@TestRestControllerAdvice(basePackages = "com.example", name = "myRestControllerAdvice")
323+
static class RestControllerAdviceClass {
324+
}
325+
227326
}

0 commit comments

Comments
 (0)