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:
@@ -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
|
||||
|
||||
@@ -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<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 {
|
||||
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 {
|
||||
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") ||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <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 —
|
||||
* 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 {
|
||||
|
||||
/**
|
||||
* 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 "";
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <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;
|
||||
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 "";
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user