diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java index 0a27be3529..903984ab0f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -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 attributesForRepeatable(AnnotationMetadata metadata, - Class annotationType, Class containerType) { + Class annotationType, Class containerType, + Predicate> predicate) { - return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, false); + return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, predicate, false, false); } static Set attributesForRepeatable(AnnotationMetadata metadata, diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index fc7d387e56..9a7917011b 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -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 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) { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java index 38cd8623c4..bae23445e5 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java @@ -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 { diff --git a/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java b/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java index f82743ab1d..73747c0fa4 100644 --- a/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java @@ -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 getMergedRepeatableAnnotationAttributes( Class annotationType, Class 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 getMergedRepeatableAnnotationAttributes( Class annotationType, Class containerType, boolean classValuesAsString, boolean sortByReversedMetaDistance) { + return getMergedRepeatableAnnotationAttributes(annotationType, containerType, + mergedAnnotation -> true, classValuesAsString, sortByReversedMetaDistance); + } + + /** + * Retrieve all repeatable annotations of the given type within the + * annotation hierarchy above the underlying element (as direct + * annotation or meta-annotation); and for each annotation found, merge that + * annotation's attributes with matching attributes from annotations + * in lower levels of the annotation hierarchy and store the results in an + * instance of {@link AnnotationAttributes}. + *

{@link org.springframework.core.annotation.AliasFor @AliasFor} semantics + * are fully supported, both within a single annotation and within annotation + * hierarchies. + *

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. + *

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 getMergedRepeatableAnnotationAttributes( + Class annotationType, Class containerType, + Predicate> predicate, boolean classValuesAsString, + boolean sortByReversedMetaDistance) { + Stream> stream = getAnnotations().stream() + .filter(predicate) .filter(MergedAnnotationPredicates.typeIn(containerType, annotationType)); if (sortByReversedMetaDistance) {