Favor local @ComponentScan annotations over meta-annotations
Work performed in conjunction with gh-30941 resulted in a regression. Specifically, prior to Spring Framework 6.1 a locally declared @ComponentScan annotation took precedence over @ComponentScan meta-annotations, which allowed "local" configuration to override "meta-present" configuration. This commit modifies the @ComponentScan search algorithm so that locally declared @ComponentScan annotations are once again favored over @ComponentScan meta-annotations (and, indirectly, composed annotations). See gh-30941 Closes gh-31704
This commit is contained in:
parent
afcd03bddc
commit
6b53f37030
|
@ -19,6 +19,7 @@ package org.springframework.context.annotation;
|
|||
import java.lang.annotation.Annotation;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
|
||||
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
|
||||
|
@ -32,6 +33,7 @@ import org.springframework.context.event.EventListenerMethodProcessor;
|
|||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
@ -281,9 +283,10 @@ public abstract class AnnotationConfigUtils {
|
|||
}
|
||||
|
||||
static Set<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,
|
||||
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType) {
|
||||
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType,
|
||||
Predicate<MergedAnnotation<? extends Annotation>> predicate) {
|
||||
|
||||
return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, false);
|
||||
return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, predicate, false, false);
|
||||
}
|
||||
|
||||
static Set<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,
|
||||
|
|
|
@ -55,6 +55,7 @@ import org.springframework.core.Ordered;
|
|||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.annotation.MergedAnnotation;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
|
@ -285,9 +286,18 @@ class ConfigurationClassParser {
|
|||
}
|
||||
}
|
||||
|
||||
// Process any @ComponentScan annotations
|
||||
// Search for locally declared @ComponentScan annotations first.
|
||||
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
|
||||
sourceClass.getMetadata(), ComponentScan.class, ComponentScans.class);
|
||||
sourceClass.getMetadata(), ComponentScan.class, ComponentScans.class,
|
||||
MergedAnnotation::isDirectlyPresent);
|
||||
|
||||
// Fall back to searching for @ComponentScan meta-annotations (which indirectly
|
||||
// includes locally declared composed annotations).
|
||||
if (componentScans.isEmpty()) {
|
||||
componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(),
|
||||
ComponentScan.class, ComponentScans.class, MergedAnnotation::isMetaPresent);
|
||||
}
|
||||
|
||||
if (!componentScans.isEmpty() &&
|
||||
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
|
||||
for (AnnotationAttributes componentScan : componentScans) {
|
||||
|
|
|
@ -137,6 +137,50 @@ class ComponentScanAnnotationIntegrationTests {
|
|||
assertContextContainsBean(ctx, "barComponent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void localAnnotationOverridesMultipleMetaAnnotations() { // gh-31704
|
||||
ApplicationContext ctx = new AnnotationConfigApplicationContext(LocalAnnotationOverridesMultipleMetaAnnotationsConfig.class);
|
||||
|
||||
assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.LocalAnnotationOverridesMultipleMetaAnnotationsConfig");
|
||||
assertContextContainsBean(ctx, "barComponent");
|
||||
|
||||
assertContextDoesNotContainBean(ctx, "simpleComponent");
|
||||
assertContextDoesNotContainBean(ctx, "configurableComponent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void localAnnotationOverridesMultipleComposedAnnotations() { // gh-31704
|
||||
ApplicationContext ctx = new AnnotationConfigApplicationContext(LocalAnnotationOverridesMultipleComposedAnnotationsConfig.class);
|
||||
|
||||
assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.LocalAnnotationOverridesMultipleComposedAnnotationsConfig");
|
||||
assertContextContainsBean(ctx, "barComponent");
|
||||
|
||||
assertContextDoesNotContainBean(ctx, "simpleComponent");
|
||||
assertContextDoesNotContainBean(ctx, "configurableComponent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void localRepeatedAnnotationsOverrideComposedAnnotations() { // gh-31704
|
||||
ApplicationContext ctx = new AnnotationConfigApplicationContext(LocalRepeatedAnnotationsOverrideComposedAnnotationsConfig.class);
|
||||
|
||||
assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.LocalRepeatedAnnotationsOverrideComposedAnnotationsConfig");
|
||||
assertContextContainsBean(ctx, "barComponent");
|
||||
assertContextContainsBean(ctx, "configurableComponent");
|
||||
|
||||
assertContextDoesNotContainBean(ctx, "simpleComponent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void localRepeatedAnnotationsInContainerOverrideComposedAnnotations() { // gh-31704
|
||||
ApplicationContext ctx = new AnnotationConfigApplicationContext(LocalRepeatedAnnotationsInContainerOverrideComposedAnnotationsConfig.class);
|
||||
|
||||
assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.LocalRepeatedAnnotationsInContainerOverrideComposedAnnotationsConfig");
|
||||
assertContextContainsBean(ctx, "barComponent");
|
||||
assertContextContainsBean(ctx, "configurableComponent");
|
||||
|
||||
assertContextDoesNotContainBean(ctx, "simpleComponent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void viaBeanRegistration() {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
|
@ -299,6 +343,20 @@ class ComponentScanAnnotationIntegrationTests {
|
|||
String[] basePackages() default {};
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ComponentScan("org.springframework.context.annotation.componentscan.simple")
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@interface MetaConfiguration1 {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ComponentScan("example.scannable_implicitbasepackage")
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@interface MetaConfiguration2 {
|
||||
}
|
||||
|
||||
@ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple")
|
||||
static class ComposedAnnotationConfig {
|
||||
}
|
||||
|
@ -308,6 +366,32 @@ class ComponentScanAnnotationIntegrationTests {
|
|||
static class MultipleComposedAnnotationsConfig {
|
||||
}
|
||||
|
||||
@MetaConfiguration1
|
||||
@MetaConfiguration2
|
||||
@ComponentScan("example.scannable.sub")
|
||||
static class LocalAnnotationOverridesMultipleMetaAnnotationsConfig {
|
||||
}
|
||||
|
||||
@ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple")
|
||||
@ComposedConfiguration2(basePackages = "example.scannable_implicitbasepackage")
|
||||
@ComponentScan("example.scannable.sub")
|
||||
static class LocalAnnotationOverridesMultipleComposedAnnotationsConfig {
|
||||
}
|
||||
|
||||
@ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple")
|
||||
@ComponentScan("example.scannable_implicitbasepackage")
|
||||
@ComponentScan("example.scannable.sub")
|
||||
static class LocalRepeatedAnnotationsOverrideComposedAnnotationsConfig {
|
||||
}
|
||||
|
||||
@ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple")
|
||||
@ComponentScans({
|
||||
@ComponentScan("example.scannable_implicitbasepackage"),
|
||||
@ComponentScan("example.scannable.sub")
|
||||
})
|
||||
static class LocalRepeatedAnnotationsInContainerOverrideComposedAnnotationsConfig {
|
||||
}
|
||||
|
||||
|
||||
static class AwareTypeFilter implements TypeFilter, EnvironmentAware,
|
||||
ResourceLoaderAware, BeanClassLoaderAware, BeanFactoryAware {
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.util.Comparator;
|
|||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
@ -181,6 +182,7 @@ public interface AnnotatedTypeMetadata {
|
|||
* or an empty set if none were found
|
||||
* @since 6.1
|
||||
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean, boolean)
|
||||
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, Predicate, boolean, boolean)
|
||||
*/
|
||||
default Set<AnnotationAttributes> getMergedRepeatableAnnotationAttributes(
|
||||
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType,
|
||||
|
@ -216,12 +218,58 @@ public interface AnnotatedTypeMetadata {
|
|||
* or an empty set if none were found
|
||||
* @since 6.1
|
||||
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean)
|
||||
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, Predicate, boolean, boolean)
|
||||
*/
|
||||
default Set<AnnotationAttributes> getMergedRepeatableAnnotationAttributes(
|
||||
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType,
|
||||
boolean classValuesAsString, boolean sortByReversedMetaDistance) {
|
||||
|
||||
return getMergedRepeatableAnnotationAttributes(annotationType, containerType,
|
||||
mergedAnnotation -> true, classValuesAsString, sortByReversedMetaDistance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all <em>repeatable annotations</em> of the given type within the
|
||||
* annotation hierarchy <em>above</em> the underlying element (as direct
|
||||
* annotation or meta-annotation); and for each annotation found, merge that
|
||||
* annotation's attributes with <em>matching</em> attributes from annotations
|
||||
* in lower levels of the annotation hierarchy and store the results in an
|
||||
* instance of {@link AnnotationAttributes}.
|
||||
* <p>{@link org.springframework.core.annotation.AliasFor @AliasFor} semantics
|
||||
* are fully supported, both within a single annotation and within annotation
|
||||
* hierarchies.
|
||||
* <p>The supplied {@link Predicate} will be used to filter the results. For
|
||||
* example, supply {@code mergedAnnotation -> true} to include all annotations
|
||||
* in the results; supply {@code MergedAnnotation::isDirectlyPresent} to limit
|
||||
* the results to directly declared annotations, etc.
|
||||
* <p>If the {@code sortByReversedMetaDistance} flag is set to {@code true},
|
||||
* the results will be sorted in {@link Comparator#reversed() reversed} order
|
||||
* based on each annotation's {@linkplain MergedAnnotation#getDistance()
|
||||
* meta distance}, which effectively orders meta-annotations before annotations
|
||||
* that are declared directly on the underlying element.
|
||||
* @param annotationType the annotation type to find
|
||||
* @param containerType the type of the container that holds the annotations
|
||||
* @param predicate a {@code Predicate} to apply to each {@code MergedAnnotation}
|
||||
* to determine if it should be included in the results
|
||||
* @param classValuesAsString whether to convert class references to {@code String}
|
||||
* class names for exposure as values in the returned {@code AnnotationAttributes},
|
||||
* instead of {@code Class} references which might potentially have to be loaded
|
||||
* first
|
||||
* @param sortByReversedMetaDistance {@code true} if the results should be
|
||||
* sorted in reversed order based on each annotation's meta distance
|
||||
* @return the set of all merged repeatable {@code AnnotationAttributes} found,
|
||||
* or an empty set if none were found
|
||||
* @since 6.1.2
|
||||
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean)
|
||||
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean, boolean)
|
||||
*/
|
||||
default Set<AnnotationAttributes> getMergedRepeatableAnnotationAttributes(
|
||||
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType,
|
||||
Predicate<MergedAnnotation<? extends Annotation>> predicate, boolean classValuesAsString,
|
||||
boolean sortByReversedMetaDistance) {
|
||||
|
||||
Stream<MergedAnnotation<Annotation>> stream = getAnnotations().stream()
|
||||
.filter(predicate)
|
||||
.filter(MergedAnnotationPredicates.typeIn(containerType, annotationType));
|
||||
|
||||
if (sortByReversedMetaDistance) {
|
||||
|
|
Loading…
Reference in New Issue