Skip to content

Commit

Permalink
Favor local @⁠ComponentScan annotations over meta-annotations
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sbrannen committed Dec 6, 2023
1 parent afcd03b commit 6b53f37
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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;
Expand All @@ -32,6 +33,7 @@
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;
Expand Down Expand Up @@ -281,9 +283,10 @@ static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, String
}

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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
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;
Expand Down Expand Up @@ -285,9 +286,18 @@ protected final SourceClass doProcessConfigurationClass(
}
}

// 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,50 @@ void multipleComposedComponentScanAnnotations() { // gh-30941
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();
Expand Down Expand Up @@ -299,6 +343,20 @@ private static void assertContextDoesNotContainBean(ApplicationContext ctx, Stri
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 {
}
Expand All @@ -308,6 +366,32 @@ static class ComposedAnnotationConfig {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;

Expand Down Expand Up @@ -181,6 +182,7 @@ default MultiValueMap<String, Object> getAllAnnotationAttributes(
* 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,
Expand Down Expand Up @@ -216,12 +218,58 @@ default Set<AnnotationAttributes> getMergedRepeatableAnnotationAttributes(
* 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) {
Expand Down

0 comments on commit 6b53f37

Please sign in to comment.