Skip to content

Commit 6b53f37

Browse files
committed
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
1 parent afcd03b commit 6b53f37

File tree

4 files changed

+149
-4
lines changed

4 files changed

+149
-4
lines changed

spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.lang.annotation.Annotation;
2020
import java.util.LinkedHashSet;
2121
import java.util.Set;
22+
import java.util.function.Predicate;
2223

2324
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
2425
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
@@ -32,6 +33,7 @@
3233
import org.springframework.context.support.GenericApplicationContext;
3334
import org.springframework.core.annotation.AnnotationAttributes;
3435
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
36+
import org.springframework.core.annotation.MergedAnnotation;
3537
import org.springframework.core.type.AnnotatedTypeMetadata;
3638
import org.springframework.core.type.AnnotationMetadata;
3739
import org.springframework.lang.Nullable;
@@ -281,9 +283,10 @@ static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, String
281283
}
282284

283285
static Set<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,
284-
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType) {
286+
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType,
287+
Predicate<MergedAnnotation<? extends Annotation>> predicate) {
285288

286-
return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, false);
289+
return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, predicate, false, false);
287290
}
288291

289292
static Set<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,

spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.springframework.core.annotation.AnnotationAttributes;
5656
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
5757
import org.springframework.core.annotation.AnnotationUtils;
58+
import org.springframework.core.annotation.MergedAnnotation;
5859
import org.springframework.core.env.ConfigurableEnvironment;
5960
import org.springframework.core.env.Environment;
6061
import org.springframework.core.io.ResourceLoader;
@@ -285,9 +286,18 @@ protected final SourceClass doProcessConfigurationClass(
285286
}
286287
}
287288

288-
// Process any @ComponentScan annotations
289+
// Search for locally declared @ComponentScan annotations first.
289290
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
290-
sourceClass.getMetadata(), ComponentScan.class, ComponentScans.class);
291+
sourceClass.getMetadata(), ComponentScan.class, ComponentScans.class,
292+
MergedAnnotation::isDirectlyPresent);
293+
294+
// Fall back to searching for @ComponentScan meta-annotations (which indirectly
295+
// includes locally declared composed annotations).
296+
if (componentScans.isEmpty()) {
297+
componentScans = AnnotationConfigUtils.attributesForRepeatable(sourceClass.getMetadata(),
298+
ComponentScan.class, ComponentScans.class, MergedAnnotation::isMetaPresent);
299+
}
300+
291301
if (!componentScans.isEmpty() &&
292302
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
293303
for (AnnotationAttributes componentScan : componentScans) {

spring-context/src/test/java/org/springframework/context/annotation/ComponentScanAnnotationIntegrationTests.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,50 @@ void multipleComposedComponentScanAnnotations() { // gh-30941
137137
assertContextContainsBean(ctx, "barComponent");
138138
}
139139

140+
@Test
141+
void localAnnotationOverridesMultipleMetaAnnotations() { // gh-31704
142+
ApplicationContext ctx = new AnnotationConfigApplicationContext(LocalAnnotationOverridesMultipleMetaAnnotationsConfig.class);
143+
144+
assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.LocalAnnotationOverridesMultipleMetaAnnotationsConfig");
145+
assertContextContainsBean(ctx, "barComponent");
146+
147+
assertContextDoesNotContainBean(ctx, "simpleComponent");
148+
assertContextDoesNotContainBean(ctx, "configurableComponent");
149+
}
150+
151+
@Test
152+
void localAnnotationOverridesMultipleComposedAnnotations() { // gh-31704
153+
ApplicationContext ctx = new AnnotationConfigApplicationContext(LocalAnnotationOverridesMultipleComposedAnnotationsConfig.class);
154+
155+
assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.LocalAnnotationOverridesMultipleComposedAnnotationsConfig");
156+
assertContextContainsBean(ctx, "barComponent");
157+
158+
assertContextDoesNotContainBean(ctx, "simpleComponent");
159+
assertContextDoesNotContainBean(ctx, "configurableComponent");
160+
}
161+
162+
@Test
163+
void localRepeatedAnnotationsOverrideComposedAnnotations() { // gh-31704
164+
ApplicationContext ctx = new AnnotationConfigApplicationContext(LocalRepeatedAnnotationsOverrideComposedAnnotationsConfig.class);
165+
166+
assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.LocalRepeatedAnnotationsOverrideComposedAnnotationsConfig");
167+
assertContextContainsBean(ctx, "barComponent");
168+
assertContextContainsBean(ctx, "configurableComponent");
169+
170+
assertContextDoesNotContainBean(ctx, "simpleComponent");
171+
}
172+
173+
@Test
174+
void localRepeatedAnnotationsInContainerOverrideComposedAnnotations() { // gh-31704
175+
ApplicationContext ctx = new AnnotationConfigApplicationContext(LocalRepeatedAnnotationsInContainerOverrideComposedAnnotationsConfig.class);
176+
177+
assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.LocalRepeatedAnnotationsInContainerOverrideComposedAnnotationsConfig");
178+
assertContextContainsBean(ctx, "barComponent");
179+
assertContextContainsBean(ctx, "configurableComponent");
180+
181+
assertContextDoesNotContainBean(ctx, "simpleComponent");
182+
}
183+
140184
@Test
141185
void viaBeanRegistration() {
142186
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
@@ -299,6 +343,20 @@ private static void assertContextDoesNotContainBean(ApplicationContext ctx, Stri
299343
String[] basePackages() default {};
300344
}
301345

346+
@Configuration
347+
@ComponentScan("org.springframework.context.annotation.componentscan.simple")
348+
@Retention(RetentionPolicy.RUNTIME)
349+
@Target(ElementType.TYPE)
350+
@interface MetaConfiguration1 {
351+
}
352+
353+
@Configuration
354+
@ComponentScan("example.scannable_implicitbasepackage")
355+
@Retention(RetentionPolicy.RUNTIME)
356+
@Target(ElementType.TYPE)
357+
@interface MetaConfiguration2 {
358+
}
359+
302360
@ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple")
303361
static class ComposedAnnotationConfig {
304362
}
@@ -308,6 +366,32 @@ static class ComposedAnnotationConfig {
308366
static class MultipleComposedAnnotationsConfig {
309367
}
310368

369+
@MetaConfiguration1
370+
@MetaConfiguration2
371+
@ComponentScan("example.scannable.sub")
372+
static class LocalAnnotationOverridesMultipleMetaAnnotationsConfig {
373+
}
374+
375+
@ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple")
376+
@ComposedConfiguration2(basePackages = "example.scannable_implicitbasepackage")
377+
@ComponentScan("example.scannable.sub")
378+
static class LocalAnnotationOverridesMultipleComposedAnnotationsConfig {
379+
}
380+
381+
@ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple")
382+
@ComponentScan("example.scannable_implicitbasepackage")
383+
@ComponentScan("example.scannable.sub")
384+
static class LocalRepeatedAnnotationsOverrideComposedAnnotationsConfig {
385+
}
386+
387+
@ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple")
388+
@ComponentScans({
389+
@ComponentScan("example.scannable_implicitbasepackage"),
390+
@ComponentScan("example.scannable.sub")
391+
})
392+
static class LocalRepeatedAnnotationsInContainerOverrideComposedAnnotationsConfig {
393+
}
394+
311395

312396
static class AwareTypeFilter implements TypeFilter, EnvironmentAware,
313397
ResourceLoaderAware, BeanClassLoaderAware, BeanFactoryAware {

spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.LinkedHashSet;
2222
import java.util.Map;
2323
import java.util.Set;
24+
import java.util.function.Predicate;
2425
import java.util.stream.Collectors;
2526
import java.util.stream.Stream;
2627

@@ -181,6 +182,7 @@ default MultiValueMap<String, Object> getAllAnnotationAttributes(
181182
* or an empty set if none were found
182183
* @since 6.1
183184
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean, boolean)
185+
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, Predicate, boolean, boolean)
184186
*/
185187
default Set<AnnotationAttributes> getMergedRepeatableAnnotationAttributes(
186188
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType,
@@ -216,12 +218,58 @@ default Set<AnnotationAttributes> getMergedRepeatableAnnotationAttributes(
216218
* or an empty set if none were found
217219
* @since 6.1
218220
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean)
221+
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, Predicate, boolean, boolean)
219222
*/
220223
default Set<AnnotationAttributes> getMergedRepeatableAnnotationAttributes(
221224
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType,
222225
boolean classValuesAsString, boolean sortByReversedMetaDistance) {
223226

227+
return getMergedRepeatableAnnotationAttributes(annotationType, containerType,
228+
mergedAnnotation -> true, classValuesAsString, sortByReversedMetaDistance);
229+
}
230+
231+
/**
232+
* Retrieve all <em>repeatable annotations</em> of the given type within the
233+
* annotation hierarchy <em>above</em> the underlying element (as direct
234+
* annotation or meta-annotation); and for each annotation found, merge that
235+
* annotation's attributes with <em>matching</em> attributes from annotations
236+
* in lower levels of the annotation hierarchy and store the results in an
237+
* instance of {@link AnnotationAttributes}.
238+
* <p>{@link org.springframework.core.annotation.AliasFor @AliasFor} semantics
239+
* are fully supported, both within a single annotation and within annotation
240+
* hierarchies.
241+
* <p>The supplied {@link Predicate} will be used to filter the results. For
242+
* example, supply {@code mergedAnnotation -> true} to include all annotations
243+
* in the results; supply {@code MergedAnnotation::isDirectlyPresent} to limit
244+
* the results to directly declared annotations, etc.
245+
* <p>If the {@code sortByReversedMetaDistance} flag is set to {@code true},
246+
* the results will be sorted in {@link Comparator#reversed() reversed} order
247+
* based on each annotation's {@linkplain MergedAnnotation#getDistance()
248+
* meta distance}, which effectively orders meta-annotations before annotations
249+
* that are declared directly on the underlying element.
250+
* @param annotationType the annotation type to find
251+
* @param containerType the type of the container that holds the annotations
252+
* @param predicate a {@code Predicate} to apply to each {@code MergedAnnotation}
253+
* to determine if it should be included in the results
254+
* @param classValuesAsString whether to convert class references to {@code String}
255+
* class names for exposure as values in the returned {@code AnnotationAttributes},
256+
* instead of {@code Class} references which might potentially have to be loaded
257+
* first
258+
* @param sortByReversedMetaDistance {@code true} if the results should be
259+
* sorted in reversed order based on each annotation's meta distance
260+
* @return the set of all merged repeatable {@code AnnotationAttributes} found,
261+
* or an empty set if none were found
262+
* @since 6.1.2
263+
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean)
264+
* @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean, boolean)
265+
*/
266+
default Set<AnnotationAttributes> getMergedRepeatableAnnotationAttributes(
267+
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType,
268+
Predicate<MergedAnnotation<? extends Annotation>> predicate, boolean classValuesAsString,
269+
boolean sortByReversedMetaDistance) {
270+
224271
Stream<MergedAnnotation<Annotation>> stream = getAnnotations().stream()
272+
.filter(predicate)
225273
.filter(MergedAnnotationPredicates.typeIn(containerType, annotationType));
226274

227275
if (sortByReversedMetaDistance) {

0 commit comments

Comments
 (0)