Skip to content

Commit 3bda2b7

Browse files
committed
Find all @componentscan and @propertysource annotations
Prior to this commit, Spring failed to find multiple composed @componentscan and @propertysource annotations or multiple @ComponentScans and @PropertySources container annotations. The reason was due to lacking support in the AnnotatedTypeMetadata API. This commit introduces support for finding all @componentscan and @propertysource annotations by making use of the new getMergedRepeatableAnnotationAttributes() method in AnnotatedTypeMetadata. Closes gh-30941 See gh-31041
1 parent 0b902f3 commit 3bda2b7

File tree

8 files changed

+89
-37
lines changed

8 files changed

+89
-37
lines changed

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

+3-27
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@
1717
package org.springframework.context.annotation;
1818

1919
import java.lang.annotation.Annotation;
20-
import java.util.Collections;
2120
import java.util.LinkedHashSet;
22-
import java.util.Map;
2321
import java.util.Set;
2422

2523
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
@@ -51,6 +49,7 @@
5149
* @author Chris Beams
5250
* @author Phillip Webb
5351
* @author Stephane Nicoll
52+
* @author Sam Brannen
5453
* @since 2.5
5554
* @see ContextAnnotationAutowireCandidateResolver
5655
* @see ConfigurationClassPostProcessor
@@ -281,33 +280,10 @@ static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, String
281280
return AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(annotationTypeName));
282281
}
283282

284-
@SuppressWarnings("unchecked")
285283
static Set<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,
286-
Class<? extends Annotation> containerType, Class<? extends Annotation> annotationType) {
284+
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType) {
287285

288-
Set<AnnotationAttributes> result = new LinkedHashSet<>();
289-
290-
// Direct annotation present or meta-present?
291-
addAttributesIfNotNull(result, metadata.getAnnotationAttributes(annotationType.getName()));
292-
293-
// Container annotation present or meta-present?
294-
Map<String, Object> container = metadata.getAnnotationAttributes(containerType.getName());
295-
if (container != null && container.containsKey("value")) {
296-
for (Map<String, Object> containedAttributes : (Map<String, Object>[]) container.get("value")) {
297-
addAttributesIfNotNull(result, containedAttributes);
298-
}
299-
}
300-
301-
// Return merged result
302-
return Collections.unmodifiableSet(result);
303-
}
304-
305-
private static void addAttributesIfNotNull(
306-
Set<AnnotationAttributes> result, @Nullable Map<String, Object> attributes) {
307-
308-
if (attributes != null) {
309-
result.add(AnnotationAttributes.fromMap(attributes));
310-
}
286+
return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, false);
311287
}
312288

313289
}

Diff for: spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -46,6 +46,10 @@
4646
*
4747
* <p>See {@link Configuration @Configuration}'s Javadoc for usage examples.
4848
*
49+
* <p>{@code @ComponentScan} can be used as a <em>{@linkplain Repeatable repeatable}</em>
50+
* annotation. {@code @ComponentScan} may also be used as a <em>meta-annotation</em>
51+
* to create custom <em>composed annotations</em> with attribute overrides.
52+
*
4953
* @author Chris Beams
5054
* @author Juergen Hoeller
5155
* @author Sam Brannen

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,8 @@ protected final SourceClass doProcessConfigurationClass(
267267

268268
// Process any @PropertySource annotations
269269
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
270-
sourceClass.getMetadata(), PropertySources.class,
271-
org.springframework.context.annotation.PropertySource.class)) {
270+
sourceClass.getMetadata(), org.springframework.context.annotation.PropertySource.class,
271+
PropertySources.class)) {
272272
if (this.propertySourceRegistry != null) {
273273
this.propertySourceRegistry.processPropertySource(propertySource);
274274
}
@@ -280,7 +280,7 @@ protected final SourceClass doProcessConfigurationClass(
280280

281281
// Process any @ComponentScan annotations
282282
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
283-
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
283+
sourceClass.getMetadata(), ComponentScan.class, ComponentScans.class);
284284
if (!componentScans.isEmpty() &&
285285
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
286286
for (AnnotationAttributes componentScan : componentScans) {

Diff for: spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java

+3-5
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,9 @@
147147
* ConfigurableEnvironment} and {@link org.springframework.core.env.MutablePropertySources
148148
* MutablePropertySources} javadocs for details.
149149
*
150-
* <p><b>NOTE: This annotation is repeatable according to Java 8 conventions.</b>
151-
* However, all such {@code @PropertySource} annotations need to be declared at the same
152-
* level: either directly on the configuration class or as meta-annotations on the
153-
* same custom annotation. Mixing direct annotations and meta-annotations is not
154-
* recommended since direct annotations will effectively override meta-annotations.
150+
* <p>{@code @PropertySource} can be used as a <em>{@linkplain Repeatable repeatable}</em>
151+
* annotation. {@code @PropertySource} may also be used as a <em>meta-annotation</em>
152+
* to create custom <em>composed annotations</em> with attribute overrides.
155153
*
156154
* @author Chris Beams
157155
* @author Juergen Hoeller

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

+29
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.springframework.beans.factory.config.BeanDefinition;
4545
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
4646
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
47+
import org.springframework.context.ApplicationContext;
4748
import org.springframework.context.EnvironmentAware;
4849
import org.springframework.context.ResourceLoaderAware;
4950
import org.springframework.context.annotation.ComponentScan.Filter;
@@ -143,6 +144,15 @@ public void viaContextRegistration_WithComposedAnnotation() {
143144
.isTrue();
144145
}
145146

147+
@Test
148+
void multipleComposedComponentScanAnnotations() { // gh-30941
149+
ApplicationContext ctx = new AnnotationConfigApplicationContext(MultipleComposedAnnotationsConfig.class);
150+
ctx.getBean(MultipleComposedAnnotationsConfig.class);
151+
assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.MultipleComposedAnnotationsConfig");
152+
assertContextContainsBean(ctx, "simpleComponent");
153+
assertContextContainsBean(ctx, "barComponent");
154+
}
155+
146156
@Test
147157
public void viaBeanRegistration() {
148158
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
@@ -267,6 +277,10 @@ public void withBasePackagesAndValueAlias() {
267277
assertThat(ctx.containsBean("fooServiceImpl")).isTrue();
268278
}
269279

280+
private static void assertContextContainsBean(ApplicationContext ctx, String beanName) {
281+
assertThat(ctx.containsBean(beanName)).as("context contains bean " + beanName).isTrue();
282+
}
283+
270284

271285
@Configuration
272286
@ComponentScan
@@ -278,10 +292,25 @@ public void withBasePackagesAndValueAlias() {
278292
String[] basePackages() default {};
279293
}
280294

295+
@Configuration
296+
@ComponentScan
297+
@Retention(RetentionPolicy.RUNTIME)
298+
@Target(ElementType.TYPE)
299+
public @interface ComposedConfiguration2 {
300+
301+
@AliasFor(annotation = ComponentScan.class)
302+
String[] basePackages() default {};
303+
}
304+
281305
@ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple")
282306
public static class ComposedAnnotationConfig {
283307
}
284308

309+
@ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple")
310+
@ComposedConfiguration2(basePackages = "example.scannable.sub")
311+
static class MultipleComposedAnnotationsConfig {
312+
}
313+
285314
public static class AwareTypeFilter implements TypeFilter, EnvironmentAware,
286315
ResourceLoaderAware, BeanClassLoaderAware, BeanFactoryAware {
287316

Diff for: spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -32,6 +32,7 @@
3232
import org.springframework.beans.factory.BeanDefinitionStoreException;
3333
import org.springframework.beans.factory.FactoryBean;
3434
import org.springframework.beans.testfixture.beans.TestBean;
35+
import org.springframework.context.ApplicationContext;
3536
import org.springframework.context.ConfigurableApplicationContext;
3637
import org.springframework.core.annotation.AliasFor;
3738
import org.springframework.core.env.Environment;
@@ -239,6 +240,14 @@ void withRepeatedPropertySourcesOnComposedAnnotation() {
239240
}
240241
}
241242

243+
@Test
244+
void multipleComposedPropertySourceAnnotations() { // gh-30941
245+
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MultipleComposedAnnotationsConfig.class);
246+
ctx.getBean(MultipleComposedAnnotationsConfig.class);
247+
assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2", "from.p3", "from.p4", "from.p5");
248+
ctx.close();
249+
}
250+
242251
@Test
243252
void withNamedPropertySources() {
244253
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithNamedPropertySources.class);
@@ -305,6 +314,17 @@ void orderingDoesntReplaceExisting() throws Exception {
305314
}
306315

307316

317+
private static void assertEnvironmentContainsProperties(ApplicationContext ctx, String... names) {
318+
for (String name : names) {
319+
assertThat(ctx.getEnvironment().containsProperty(name)).as("environment contains property " + name).isTrue();
320+
}
321+
}
322+
323+
private static void assertEnvironmentContainsProperty(ApplicationContext ctx, String name) {
324+
assertThat(ctx.getEnvironment().containsProperty(name)).as("environment contains property " + name).isTrue();
325+
}
326+
327+
308328
@Configuration
309329
@PropertySource("classpath:${unresolvable}/p1.properties")
310330
static class ConfigWithUnresolvablePlaceholder {
@@ -496,6 +516,28 @@ static class ConfigWithRepeatedPropertySourceAnnotations {
496516
static class ConfigWithRepeatedPropertySourceAnnotationsOnComposedAnnotation {
497517
}
498518

519+
@Retention(RetentionPolicy.RUNTIME)
520+
@PropertySource("classpath:org/springframework/context/annotation/p1.properties")
521+
@interface PropertySource1 {
522+
}
523+
524+
@Retention(RetentionPolicy.RUNTIME)
525+
@PropertySource("classpath:org/springframework/context/annotation/p2.properties")
526+
@PropertySources({
527+
@PropertySource("classpath:org/springframework/context/annotation/p3.properties"),
528+
})
529+
@interface PropertySource23 {
530+
}
531+
532+
@Configuration
533+
@PropertySource1
534+
@PropertySource23
535+
@PropertySources({
536+
@PropertySource("classpath:org/springframework/context/annotation/p4.properties")
537+
})
538+
@PropertySource("classpath:org/springframework/context/annotation/p5.properties")
539+
static class MultipleComposedAnnotationsConfig {
540+
}
499541

500542
@Configuration
501543
@PropertySources({
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
testbean.name=p4TestBean
2+
from.p4=p4Value
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
testbean.name=p5TestBean
2+
from.p5=p5Value

0 commit comments

Comments
 (0)