Browse Source

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
pull/31121/head
Sam Brannen 2 years ago
parent
commit
ff104b6de0
  1. 15
      framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc
  2. 41
      spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java
  3. 1
      spring-context/src/main/java/org/springframework/context/annotation/Configuration.java
  4. 14
      spring-context/src/main/java/org/springframework/stereotype/Component.java
  5. 6
      spring-context/src/main/java/org/springframework/stereotype/Controller.java
  6. 11
      spring-context/src/main/java/org/springframework/stereotype/Repository.java
  7. 6
      spring-context/src/main/java/org/springframework/stereotype/Service.java
  8. 103
      spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java

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

@ -665,9 +665,18 @@ analogous to how the container selects between multiple `@Autowired` constructor @@ -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

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

@ -17,6 +17,7 @@ @@ -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; @@ -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 { @@ -100,6 +102,12 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator {
protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) {
AnnotationMetadata metadata = annotatedDef.getMetadata();
Set<String> 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 { @@ -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<String> 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 { @@ -134,8 +172,7 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator {
protected boolean isStereotypeWithNameValue(String annotationType,
Set<String> metaAnnotationTypes, @Nullable Map<String, Object> 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") ||

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

@ -434,6 +434,7 @@ public @interface Configuration { @@ -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.
* <p>Alias for {@link Component#value}.
* @return the explicit component name, if any (or empty String otherwise)
* @see AnnotationBeanNameGenerator
*/

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

@ -1,5 +1,5 @@ @@ -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; @@ -28,11 +28,17 @@ import java.lang.annotation.Target;
* when using annotation-based configuration and classpath scanning.
*
* <p>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 &mdash;
* for example, the {@link Repository @Repository} annotation or AspectJ's
* {@link org.aspectj.lang.annotation.Aspect @Aspect} annotation.
*
* <p>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 { @@ -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 "";

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

@ -1,5 +1,5 @@ @@ -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; @@ -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 "";

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

@ -1,5 +1,5 @@ @@ -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; @@ -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.
*
* <p>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.
* <p>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; @@ -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 "";

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

@ -1,5 +1,5 @@ @@ -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; @@ -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 "";

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

@ -20,6 +20,7 @@ import java.lang.annotation.ElementType; @@ -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 @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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.
* <p>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.
* <p>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 {
}
}

Loading…
Cancel
Save