diff --git a/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc b/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc index 47587713d30..37c61a1208d 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc @@ -665,9 +665,18 @@ analogous to how the container selects between multiple `@Autowired` constructor When a component is autodetected as part of the scanning process, its bean name is generated by the `BeanNameGenerator` strategy known to that scanner. By default, any -Spring stereotype annotation (`@Component`, `@Repository`, `@Service`, and -`@Controller`) that contains a name `value` thereby provides that name to the -corresponding bean definition. +Spring stereotype annotation (`@Component`, `@Repository`, `@Service`, `@Controller`, +`@Configuration`, and so forth) that contains a non-empty `value` attribute provides that +value as the name to the corresponding bean definition. + +[NOTE] +==== +As of Spring Framework 6.1, the name of the annotation attribute that is used to specify +the bean name is no longer required to be `value`. Custom stereotype annotations can +declare an attribute with a different name (such as `name`) and annotate that attribute +with `@AliasFor(annotation = Component.class, attribute = "value")`. See the source code +declaration of `Repository#value()` for a concrete example. +==== If such an annotation contains no name `value` or for any other detected component (such as those discovered by custom filters), the default bean name generator returns 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 d3508028993..e43425b8345 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.util.Collections; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -26,6 +27,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.AnnotationAttributes; +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.type.AnnotationMetadata; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -100,6 +102,12 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator { protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) { AnnotationMetadata metadata = annotatedDef.getMetadata(); Set annotationTypes = metadata.getAnnotationTypes(); + + String explicitBeanName = getExplicitBeanName(metadata); + if (explicitBeanName != null) { + return explicitBeanName; + } + String beanName = null; for (String annotationType : annotationTypes) { AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(metadata, annotationType); @@ -123,6 +131,36 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator { return beanName; } + /** + * Get the explicit bean name for the underlying class, as configured via + * {@link org.springframework.stereotype.Component @Component} and taking into + * account {@link org.springframework.core.annotation.AliasFor @AliasFor} + * semantics for annotation attribute overrides for {@code @Component}'s + * {@code value} attribute. + * @param metadata the {@link AnnotationMetadata} for the underlying class + * @return the explicit bean name, or {@code null} if not found + * @since 6.1 + * @see org.springframework.stereotype.Component#value() + */ + @Nullable + private String getExplicitBeanName(AnnotationMetadata metadata) { + List names = metadata.getAnnotations().stream(COMPONENT_ANNOTATION_CLASSNAME) + .map(annotation -> annotation.getString(MergedAnnotation.VALUE)) + .filter(StringUtils::hasText) + .map(String::trim) + .distinct() + .toList(); + + if (names.size() == 1) { + return names.get(0); + } + if (names.size() > 1) { + throw new IllegalStateException( + "Stereotype annotations suggest inconsistent component names: " + names); + } + return null; + } + /** * Check whether the given annotation is a stereotype that is allowed * to suggest a component name through its {@code value()} attribute. @@ -134,8 +172,7 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator { protected boolean isStereotypeWithNameValue(String annotationType, Set metaAnnotationTypes, @Nullable Map attributes) { - boolean isStereotype = annotationType.equals(COMPONENT_ANNOTATION_CLASSNAME) || - metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) || + boolean isStereotype = metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) || annotationType.equals("jakarta.annotation.ManagedBean") || annotationType.equals("javax.annotation.ManagedBean") || annotationType.equals("jakarta.inject.Named") || diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java b/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java index 0c3bbe2193b..23170a5f4d7 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java @@ -434,6 +434,7 @@ public @interface Configuration { * {@link AnnotationConfigApplicationContext}. If the {@code @Configuration} class * is registered as a traditional XML bean definition, the name/id of the bean * element will take precedence. + *

Alias for {@link Component#value}. * @return the explicit component name, if any (or empty String otherwise) * @see AnnotationBeanNameGenerator */ diff --git a/spring-context/src/main/java/org/springframework/stereotype/Component.java b/spring-context/src/main/java/org/springframework/stereotype/Component.java index a6e09edc6e7..3d679bb4061 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Component.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Component.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2023 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. @@ -28,11 +28,17 @@ import java.lang.annotation.Target; * when using annotation-based configuration and classpath scanning. * *

Other class-level annotations may be considered as identifying - * a component as well, typically a special kind of component: - * e.g. the {@link Repository @Repository} annotation or AspectJ's + * a component as well, typically a special kind of component — + * for example, the {@link Repository @Repository} annotation or AspectJ's * {@link org.aspectj.lang.annotation.Aspect @Aspect} annotation. * + *

As of Spring Framework 6.1, custom component stereotype annotations should + * use {@link org.springframework.core.annotation.AliasFor @AliasFor} to declare + * an explicit alias for this annotation's {@link #value} attribute. See the + * source code declaration of {@link Repository#value()} for a concrete example. + * * @author Mark Fisher + * @author Sam Brannen * @since 2.5 * @see Repository * @see Service @@ -47,7 +53,7 @@ public @interface Component { /** * The value may indicate a suggestion for a logical component name, - * to be turned into a Spring bean in case of an autodetected component. + * to be turned into a Spring bean name in case of an autodetected component. * @return the suggested component name, if any (or empty String otherwise) */ String value() default ""; diff --git a/spring-context/src/main/java/org/springframework/stereotype/Controller.java b/spring-context/src/main/java/org/springframework/stereotype/Controller.java index 6629feea4b3..a991c31142e 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Controller.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Controller.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -46,9 +46,7 @@ import org.springframework.core.annotation.AliasFor; public @interface Controller { /** - * The value may indicate a suggestion for a logical component name, - * to be turned into a Spring bean in case of an autodetected component. - * @return the suggested component name, if any (or empty String otherwise) + * Alias for {@link Component#value}. */ @AliasFor(annotation = Component.class) String value() default ""; diff --git a/spring-context/src/main/java/org/springframework/stereotype/Repository.java b/spring-context/src/main/java/org/springframework/stereotype/Repository.java index 22e54ab8cd0..5a964bbd063 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Repository.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Repository.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -43,9 +43,8 @@ import org.springframework.core.annotation.AliasFor; * to its role in the overall application architecture for the purpose of tooling, * aspects, etc. * - *

As of Spring 2.5, this annotation also serves as a specialization of - * {@link Component @Component}, allowing for implementation classes to be autodetected - * through classpath scanning. + *

This annotation also serves as a specialization of {@link Component @Component}, + * allowing for implementation classes to be autodetected through classpath scanning. * * @author Rod Johnson * @author Juergen Hoeller @@ -62,9 +61,7 @@ import org.springframework.core.annotation.AliasFor; public @interface Repository { /** - * The value may indicate a suggestion for a logical component name, - * to be turned into a Spring bean in case of an autodetected component. - * @return the suggested component name, if any (or empty String otherwise) + * Alias for {@link Component#value}. */ @AliasFor(annotation = Component.class) String value() default ""; diff --git a/spring-context/src/main/java/org/springframework/stereotype/Service.java b/spring-context/src/main/java/org/springframework/stereotype/Service.java index 5c714540bbc..92a9a70601f 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Service.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Service.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -48,9 +48,7 @@ import org.springframework.core.annotation.AliasFor; public @interface Service { /** - * The value may indicate a suggestion for a logical component name, - * to be turned into a Spring bean in case of an autodetected component. - * @return the suggested component name, if any (or empty String otherwise) + * Alias for {@link Component#value}. */ @AliasFor(annotation = Component.class) String value() default ""; 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 bded664c3f9..b8b31e4fb68 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 @@ -20,6 +20,7 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.List; import example.scannable.DefaultNamedComponent; import example.scannable.JakartaManagedBeanComponent; @@ -33,6 +34,7 @@ import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefiniti import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.SimpleBeanDefinitionRegistry; +import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Service; @@ -73,20 +75,34 @@ class AnnotationBeanNameGeneratorTests { assertGeneratedNameIsDefault(ComponentWithBlankName.class); } + @Test + void generateBeanNameForConventionBasedComponentWithDuplicateIdenticalNames() { + assertGeneratedName(ConventionBasedComponentWithDuplicateIdenticalNames.class, "myComponent"); + } + @Test void generateBeanNameForComponentWithDuplicateIdenticalNames() { assertGeneratedName(ComponentWithDuplicateIdenticalNames.class, "myComponent"); } @Test - void generateBeanNameForComponentWithConflictingNames() { - BeanDefinition bd = annotatedBeanDef(ComponentWithMultipleConflictingNames.class); + void generateBeanNameForConventionBasedComponentWithConflictingNames() { + BeanDefinition bd = annotatedBeanDef(ConventionBasedComponentWithMultipleConflictingNames.class); assertThatIllegalStateException() .isThrownBy(() -> generateBeanName(bd)) .withMessage("Stereotype annotations suggest inconsistent component names: '%s' versus '%s'", "myComponent", "myService"); } + @Test + void generateBeanNameForComponentWithConflictingNames() { + BeanDefinition bd = annotatedBeanDef(ComponentWithMultipleConflictingNames.class); + assertThatIllegalStateException() + .isThrownBy(() -> generateBeanName(bd)) + .withMessage("Stereotype annotations suggest inconsistent component names: " + + List.of("myComponent", "myService")); + } + @Test void generateBeanNameWithJakartaNamedComponent() { assertGeneratedName(JakartaNamedComponent.class, "myJakartaNamedComponent"); @@ -142,6 +158,16 @@ class AnnotationBeanNameGeneratorTests { assertGeneratedName(ComposedControllerAnnotationWithStringValue.class, "restController"); } + @Test // gh-31089 + void generateBeanNameFromStereotypeAnnotationWithStringArrayValueAndExplicitComponentNameAlias() { + assertGeneratedName(ControllerAdviceClass.class, "myControllerAdvice"); + } + + @Test // gh-31089 + void generateBeanNameFromSubStereotypeAnnotationWithStringArrayValueAndExplicitComponentNameAlias() { + assertGeneratedName(RestControllerAdviceClass.class, "myRestControllerAdvice"); + } + private void assertGeneratedName(Class clazz, String expectedName) { BeanDefinition bd = annotatedBeanDef(clazz); @@ -181,6 +207,28 @@ class AnnotationBeanNameGeneratorTests { static class ComponentWithMultipleConflictingNames { } + @Retention(RetentionPolicy.RUNTIME) + @Component + @interface ConventionBasedComponent1 { + String value() default ""; + } + + @Retention(RetentionPolicy.RUNTIME) + @Component + @interface ConventionBasedComponent2 { + String value() default ""; + } + + @ConventionBasedComponent1("myComponent") + @ConventionBasedComponent2("myComponent") + static class ConventionBasedComponentWithDuplicateIdenticalNames { + } + + @ConventionBasedComponent1("myComponent") + @ConventionBasedComponent2("myService") + static class ConventionBasedComponentWithMultipleConflictingNames { + } + @Component private static class AnonymousComponent { } @@ -224,4 +272,55 @@ class AnnotationBeanNameGeneratorTests { static class ComposedControllerAnnotationWithStringValue { } + /** + * Mock of {@code org.springframework.web.bind.annotation.ControllerAdvice}, + * which also has a {@code value} attribute that is NOT a {@code String} that + * is meant to be used for the component name. + *

Declares a custom {@link #name} that explicitly aliases {@link Component#value()}. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Component + @interface TestControllerAdvice { + + @AliasFor(annotation = Component.class, attribute = "value") + String name() default ""; + + @AliasFor("basePackages") + String[] value() default {}; + + @AliasFor("value") + String[] basePackages() default {}; + } + + /** + * Mock of {@code org.springframework.web.bind.annotation.RestControllerAdvice}, + * which also has a {@code value} attribute that is NOT a {@code String} that + * is meant to be used for the component name. + *

Declares a custom {@link #name} that explicitly aliases + * {@link TestControllerAdvice#name()} instead of {@link Component#value()}. + */ + @Retention(RetentionPolicy.RUNTIME) + @TestControllerAdvice + @interface TestRestControllerAdvice { + + @AliasFor(annotation = TestControllerAdvice.class) + String name() default ""; + + @AliasFor(annotation = TestControllerAdvice.class) + String[] value() default {}; + + @AliasFor(annotation = TestControllerAdvice.class) + String[] basePackages() default {}; + } + + + @TestControllerAdvice(basePackages = "com.example", name = "myControllerAdvice") + static class ControllerAdviceClass { + } + + @TestRestControllerAdvice(basePackages = "com.example", name = "myRestControllerAdvice") + static class RestControllerAdviceClass { + } + }