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
This commit is contained in:
Sam Brannen
2023-08-22 13:10:52 +02:00
parent d189e169cc
commit ff104b6de0
8 changed files with 171 additions and 26 deletions

View File

@@ -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,18 +75,32 @@ class AnnotationBeanNameGeneratorTests {
assertGeneratedNameIsDefault(ComponentWithBlankName.class);
}
@Test
void generateBeanNameForConventionBasedComponentWithDuplicateIdenticalNames() {
assertGeneratedName(ConventionBasedComponentWithDuplicateIdenticalNames.class, "myComponent");
}
@Test
void generateBeanNameForComponentWithDuplicateIdenticalNames() {
assertGeneratedName(ComponentWithDuplicateIdenticalNames.class, "myComponent");
}
@Test
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: '%s' versus '%s'",
"myComponent", "myService");
.withMessage("Stereotype annotations suggest inconsistent component names: " +
List.of("myComponent", "myService"));
}
@Test
@@ -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.
* <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 {
}
}