From 3bda2b71242600ccb04bc4597780115036035f16 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 6 Aug 2023 12:02:08 +0300 Subject: [PATCH] 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 --- .../annotation/AnnotationConfigUtils.java | 30 ++----------- .../context/annotation/ComponentScan.java | 6 ++- .../annotation/ConfigurationClassParser.java | 6 +-- .../context/annotation/PropertySource.java | 8 ++-- ...mponentScanAnnotationIntegrationTests.java | 29 ++++++++++++ .../PropertySourceAnnotationTests.java | 44 ++++++++++++++++++- .../context/annotation/p4.properties | 1 + .../context/annotation/p5.properties | 2 + 8 files changed, 89 insertions(+), 37 deletions(-) create mode 100644 spring-context/src/test/resources/org/springframework/context/annotation/p5.properties 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 af6986d8aee..77238c28bc2 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 @@ -17,9 +17,7 @@ package org.springframework.context.annotation; import java.lang.annotation.Annotation; -import java.util.Collections; import java.util.LinkedHashSet; -import java.util.Map; import java.util.Set; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; @@ -51,6 +49,7 @@ import org.springframework.util.ClassUtils; * @author Chris Beams * @author Phillip Webb * @author Stephane Nicoll + * @author Sam Brannen * @since 2.5 * @see ContextAnnotationAutowireCandidateResolver * @see ConfigurationClassPostProcessor @@ -281,33 +280,10 @@ public abstract class AnnotationConfigUtils { return AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(annotationTypeName)); } - @SuppressWarnings("unchecked") static Set attributesForRepeatable(AnnotationMetadata metadata, - Class containerType, Class annotationType) { + Class annotationType, Class containerType) { - Set result = new LinkedHashSet<>(); - - // Direct annotation present or meta-present? - addAttributesIfNotNull(result, metadata.getAnnotationAttributes(annotationType.getName())); - - // Container annotation present or meta-present? - Map container = metadata.getAnnotationAttributes(containerType.getName()); - if (container != null && container.containsKey("value")) { - for (Map containedAttributes : (Map[]) container.get("value")) { - addAttributesIfNotNull(result, containedAttributes); - } - } - - // Return merged result - return Collections.unmodifiableSet(result); - } - - private static void addAttributesIfNotNull( - Set result, @Nullable Map attributes) { - - if (attributes != null) { - result.add(AnnotationAttributes.fromMap(attributes)); - } + return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, false); } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java index ffa1f1a0645..6b9bd448d25 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScan.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,6 +46,10 @@ import org.springframework.core.type.filter.TypeFilter; * *

See {@link Configuration @Configuration}'s Javadoc for usage examples. * + *

{@code @ComponentScan} can be used as a {@linkplain Repeatable repeatable} + * annotation. {@code @ComponentScan} may also be used as a meta-annotation + * to create custom composed annotations with attribute overrides. + * * @author Chris Beams * @author Juergen Hoeller * @author Sam Brannen 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 59d1f07147b..89550a16c1a 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 @@ -267,8 +267,8 @@ class ConfigurationClassParser { // Process any @PropertySource annotations for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( - sourceClass.getMetadata(), PropertySources.class, - org.springframework.context.annotation.PropertySource.class)) { + sourceClass.getMetadata(), org.springframework.context.annotation.PropertySource.class, + PropertySources.class)) { if (this.propertySourceRegistry != null) { this.propertySourceRegistry.processPropertySource(propertySource); } @@ -280,7 +280,7 @@ class ConfigurationClassParser { // Process any @ComponentScan annotations Set componentScans = AnnotationConfigUtils.attributesForRepeatable( - sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); + sourceClass.getMetadata(), ComponentScan.class, ComponentScans.class); if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { for (AnnotationAttributes componentScan : componentScans) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java b/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java index 2dc039b9403..2f7ae8a5e5a 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/PropertySource.java @@ -147,11 +147,9 @@ import org.springframework.core.io.support.PropertySourceFactory; * ConfigurableEnvironment} and {@link org.springframework.core.env.MutablePropertySources * MutablePropertySources} javadocs for details. * - *

NOTE: This annotation is repeatable according to Java 8 conventions. - * However, all such {@code @PropertySource} annotations need to be declared at the same - * level: either directly on the configuration class or as meta-annotations on the - * same custom annotation. Mixing direct annotations and meta-annotations is not - * recommended since direct annotations will effectively override meta-annotations. + *

{@code @PropertySource} can be used as a {@linkplain Repeatable repeatable} + * annotation. {@code @PropertySource} may also be used as a meta-annotation + * to create custom composed annotations with attribute overrides. * * @author Chris Beams * @author Juergen Hoeller 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 08f45994289..0aec519fa65 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 @@ -44,6 +44,7 @@ import org.springframework.beans.factory.annotation.CustomAutowireConfigurer; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.ComponentScan.Filter; @@ -143,6 +144,15 @@ public class ComponentScanAnnotationIntegrationTests { .isTrue(); } + @Test + void multipleComposedComponentScanAnnotations() { // gh-30941 + ApplicationContext ctx = new AnnotationConfigApplicationContext(MultipleComposedAnnotationsConfig.class); + ctx.getBean(MultipleComposedAnnotationsConfig.class); + assertContextContainsBean(ctx, "componentScanAnnotationIntegrationTests.MultipleComposedAnnotationsConfig"); + assertContextContainsBean(ctx, "simpleComponent"); + assertContextContainsBean(ctx, "barComponent"); + } + @Test public void viaBeanRegistration() { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); @@ -267,6 +277,10 @@ public class ComponentScanAnnotationIntegrationTests { assertThat(ctx.containsBean("fooServiceImpl")).isTrue(); } + private static void assertContextContainsBean(ApplicationContext ctx, String beanName) { + assertThat(ctx.containsBean(beanName)).as("context contains bean " + beanName).isTrue(); + } + @Configuration @ComponentScan @@ -278,10 +292,25 @@ public class ComponentScanAnnotationIntegrationTests { String[] basePackages() default {}; } + @Configuration + @ComponentScan + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface ComposedConfiguration2 { + + @AliasFor(annotation = ComponentScan.class) + String[] basePackages() default {}; + } + @ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple") public static class ComposedAnnotationConfig { } + @ComposedConfiguration(basePackages = "org.springframework.context.annotation.componentscan.simple") + @ComposedConfiguration2(basePackages = "example.scannable.sub") + static class MultipleComposedAnnotationsConfig { + } + public static class AwareTypeFilter implements TypeFilter, EnvironmentAware, ResourceLoaderAware, BeanClassLoaderAware, BeanFactoryAware { diff --git a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java index 4ae9279811a..b634109e86c 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/PropertySourceAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.testfixture.beans.TestBean; +import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.annotation.AliasFor; import org.springframework.core.env.Environment; @@ -239,6 +240,14 @@ class PropertySourceAnnotationTests { } } + @Test + void multipleComposedPropertySourceAnnotations() { // gh-30941 + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MultipleComposedAnnotationsConfig.class); + ctx.getBean(MultipleComposedAnnotationsConfig.class); + assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2", "from.p3", "from.p4", "from.p5"); + ctx.close(); + } + @Test void withNamedPropertySources() { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigWithNamedPropertySources.class); @@ -305,6 +314,17 @@ class PropertySourceAnnotationTests { } + private static void assertEnvironmentContainsProperties(ApplicationContext ctx, String... names) { + for (String name : names) { + assertThat(ctx.getEnvironment().containsProperty(name)).as("environment contains property " + name).isTrue(); + } + } + + private static void assertEnvironmentContainsProperty(ApplicationContext ctx, String name) { + assertThat(ctx.getEnvironment().containsProperty(name)).as("environment contains property " + name).isTrue(); + } + + @Configuration @PropertySource("classpath:${unresolvable}/p1.properties") static class ConfigWithUnresolvablePlaceholder { @@ -496,6 +516,28 @@ class PropertySourceAnnotationTests { static class ConfigWithRepeatedPropertySourceAnnotationsOnComposedAnnotation { } + @Retention(RetentionPolicy.RUNTIME) + @PropertySource("classpath:org/springframework/context/annotation/p1.properties") + @interface PropertySource1 { + } + + @Retention(RetentionPolicy.RUNTIME) + @PropertySource("classpath:org/springframework/context/annotation/p2.properties") + @PropertySources({ + @PropertySource("classpath:org/springframework/context/annotation/p3.properties"), + }) + @interface PropertySource23 { + } + + @Configuration + @PropertySource1 + @PropertySource23 + @PropertySources({ + @PropertySource("classpath:org/springframework/context/annotation/p4.properties") + }) + @PropertySource("classpath:org/springframework/context/annotation/p5.properties") + static class MultipleComposedAnnotationsConfig { + } @Configuration @PropertySources({ diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/p4.properties b/spring-context/src/test/resources/org/springframework/context/annotation/p4.properties index a9fbccd9826..8e65e9fc6d8 100644 --- a/spring-context/src/test/resources/org/springframework/context/annotation/p4.properties +++ b/spring-context/src/test/resources/org/springframework/context/annotation/p4.properties @@ -1 +1,2 @@ testbean.name=p4TestBean +from.p4=p4Value diff --git a/spring-context/src/test/resources/org/springframework/context/annotation/p5.properties b/spring-context/src/test/resources/org/springframework/context/annotation/p5.properties new file mode 100644 index 00000000000..765adc0d34c --- /dev/null +++ b/spring-context/src/test/resources/org/springframework/context/annotation/p5.properties @@ -0,0 +1,2 @@ +testbean.name=p5TestBean +from.p5=p5Value