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 ebae073973..a60f78ef2e 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc @@ -27,7 +27,7 @@ use these features. == `@Component` and Further Stereotype Annotations The `@Repository` annotation is a marker for any class that fulfills the role or -stereotype of a repository (also known as Data Access Object or DAO). Among the uses +_stereotype_ of a repository (also known as Data Access Object or DAO). Among the uses of this marker is the automatic translation of exceptions, as described in xref:data-access/orm/general.adoc#orm-exception-translation[Exception Translation]. @@ -39,7 +39,7 @@ layers, respectively). Therefore, you can annotate your component classes with `@Component`, but, by annotating them with `@Repository`, `@Service`, or `@Controller` instead, your classes are more properly suited for processing by tools or associating with aspects. For example, these stereotype annotations make ideal targets for -pointcuts. `@Repository`, `@Service`, and `@Controller` can also +pointcuts. `@Repository`, `@Service`, and `@Controller` may also carry additional semantics in future releases of the Spring Framework. Thus, if you are choosing between using `@Component` or `@Service` for your service layer, `@Service` is clearly the better choice. Similarly, as stated earlier, `@Repository` is already @@ -664,24 +664,36 @@ analogous to how the container selects between multiple `@Autowired` constructor == Naming Autodetected Components 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`, `@Controller`, -`@Configuration`, and so forth) that contains a non-empty `value` attribute provides that -value as the name to the corresponding bean definition. +generated by the `BeanNameGenerator` strategy known to that scanner. + +By default, the `AnnotationBeanNameGenerator` is used. For Spring +xref:core/beans/classpath-scanning.adoc#beans-stereotype-annotations[stereotype annotations], +if you supply a name via the the annotation's `value` attribute that name will be used as +the name in the corresponding bean definition. This convention also applies when the +following JSR-250 and JSR-330 annotations are used instead of Spring stereotype +annotations: `@jakarta.annotation.ManagedBean`, `@javax.annotation.ManagedBean`, +`@jakarta.inject.Named`, and `@javax.inject.Named`. -[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()` and `ControllerAdvice#name()` for concrete examples. +declaration of `ControllerAdvice#name()` for a concrete example. + +[WARNING] +==== +As of Spring Framework 6.1, support for convention-based stereotype names is deprecated +and will be removed in a future version of the framework. Consequently, custom stereotype +annotations must use `@AliasFor` to declare an explicit alias for the `value` attribute +in `@Component`. See the source code declaration of `Repository#value()` and +`ControllerAdvice#name()` for concrete examples. ==== -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 -the uncapitalized non-qualified class name. For example, if the following component -classes were detected, the names would be `myMovieLister` and `movieFinderImpl`: +If an explicit bean name cannot be derived from such an annotation or for any other +detected component (such as those discovered by custom filters), the default bean name +generator returns the uncapitalized non-qualified class name. For example, if the +following component classes were detected, the names would be `myMovieLister` and +`movieFinderImpl`. [tabs] ====== 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 1e6221424a..64aa0a2f26 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 @@ -22,6 +22,9 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -77,6 +80,18 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator { private static final String COMPONENT_ANNOTATION_CLASSNAME = "org.springframework.stereotype.Component"; + /** + * Set used to track which stereotype annotations have already been checked + * to see if they use a convention-based override for the {@code value} + * attribute in {@code @Component}. + * @since 6.1 + * @see #determineBeanNameFromAnnotation(AnnotatedBeanDefinition) + */ + private static final Set conventionBasedStereotypeCheckCache = ConcurrentHashMap.newKeySet(); + + + private final Log logger = LogFactory.getLog(AnnotationBeanNameGenerator.class); + private final Map> metaAnnotationTypesCache = new ConcurrentHashMap<>(); @@ -117,6 +132,15 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator { if (isStereotypeWithNameValue(annotationType, metaAnnotationTypes, attributes)) { Object value = attributes.get("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 (beanName != null && !currentName.equals(beanName)) { throw new IllegalStateException("Stereotype annotations suggest inconsistent " + "component names: '" + beanName + "' versus '" + currentName + "'"); 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 94613659f9..abf53ecd6b 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/Component.java +++ b/spring-context/src/main/java/org/springframework/stereotype/Component.java @@ -23,19 +23,35 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * Indicates that an annotated class is a "component". - * Such classes are considered as candidates for auto-detection + * Indicates that the annotated class is a component. + * + *

Such classes are considered as candidates for auto-detection * when using annotation-based configuration and classpath scanning. * + *

A component may optionally specify a logical component name via the + * {@link #value value} attribute of this annotation. + * *

Other class-level annotations may be considered as identifying * 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. + * {@link org.aspectj.lang.annotation.Aspect @Aspect} annotation. Note, however, + * that the {@code @Aspect} annotation does not automatically make a class + * eligible for classpath scanning. + * + *

Any annotation meta-annotated with {@code @Component} is considered a + * stereotype annotation which makes the annotated class eligible for + * classpath scanning. For example, {@link Service @Service}, + * {@link Controller @Controller}, and {@link Repository @Repository} are + * stereotype annotations. Stereotype annotations may also support configuration + * of a logical component name by overriding the {@link #value} attribute of this + * annotation via {@link org.springframework.core.annotation.AliasFor @AliasFor}. * - *

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()} and + *

As of Spring Framework 6.1, support for configuring the name of a stereotype + * component by convention (i.e., via a {@code String value()} attribute without + * {@code @AliasFor}) is deprecated and will be removed in a future version of the + * framework. Consequently, custom stereotype annotations must use {@code @AliasFor} + * to declare an explicit alias for this annotation's {@link #value} attribute. + * See the source code declaration of {@link Repository#value()} and * {@link org.springframework.web.bind.annotation.ControllerAdvice#name() * ControllerAdvice.name()} for concrete examples. * diff --git a/spring-context/src/test/java/example/profilescan/DevComponent.java b/spring-context/src/test/java/example/profilescan/DevComponent.java index ec52b777f9..6926102be8 100644 --- a/spring-context/src/test/java/example/profilescan/DevComponent.java +++ b/spring-context/src/test/java/example/profilescan/DevComponent.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -22,6 +22,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.context.annotation.Profile; +import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Component; @Retention(RetentionPolicy.RUNTIME) @@ -32,6 +33,7 @@ public @interface DevComponent { String PROFILE_NAME = "dev"; + @AliasFor(annotation = Component.class) String value() default ""; } diff --git a/spring-context/src/test/java/example/scannable/CustomStereotype.java b/spring-context/src/test/java/example/scannable/CustomStereotype.java index 958f7f9af6..c0b3024ecb 100644 --- a/spring-context/src/test/java/example/scannable/CustomStereotype.java +++ b/spring-context/src/test/java/example/scannable/CustomStereotype.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2009 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. @@ -22,6 +22,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.context.annotation.Scope; +import org.springframework.core.annotation.AliasFor; import org.springframework.stereotype.Service; /** @@ -33,6 +34,7 @@ import org.springframework.stereotype.Service; @Scope("prototype") public @interface CustomStereotype { + @AliasFor(annotation = Service.class) String value() default "thoreau"; } 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 b8b31e4fb6..2025342d7f 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,12 +210,16 @@ class AnnotationBeanNameGeneratorTests { @Retention(RetentionPolicy.RUNTIME) @Component @interface ConventionBasedComponent1 { + // This intentionally convention-based. Please do not add @AliasFor. + // See gh-31093. String value() default ""; } @Retention(RetentionPolicy.RUNTIME) @Component @interface ConventionBasedComponent2 { + // This intentionally convention-based. Please do not add @AliasFor. + // See gh-31093. String value() default ""; } @@ -256,7 +260,8 @@ class AnnotationBeanNameGeneratorTests { @Target(ElementType.TYPE) @Controller @interface TestRestController { - + // This intentionally convention-based. Please do not add @AliasFor. + // See gh-31093. String value() default ""; } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationMetaAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationMetaAnnotationTests.java index 7e97aaa2d0..4085cf39b4 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationMetaAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/configuration/ConfigurationMetaAnnotationTests.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. @@ -25,6 +25,7 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AliasFor; import static org.assertj.core.api.Assertions.assertThat; @@ -62,9 +63,12 @@ class ConfigurationMetaAnnotationTests { } - @Configuration @Retention(RetentionPolicy.RUNTIME) + @Configuration @interface TestConfiguration { + + @AliasFor(annotation = Configuration.class) String value() default ""; } + }