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 77238c28bc..0a27be3529 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 @@ -286,4 +286,11 @@ public abstract class AnnotationConfigUtils { return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, false); } + static Set attributesForRepeatable(AnnotationMetadata metadata, + Class annotationType, Class containerType, + boolean sortByReversedMetaDistance) { + + return metadata.getMergedRepeatableAnnotationAttributes(annotationType, containerType, false, sortByReversedMetaDistance); + } + } 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 89550a16c1..2a6bae79fc 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 @@ -268,7 +268,7 @@ class ConfigurationClassParser { // Process any @PropertySource annotations for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), org.springframework.context.annotation.PropertySource.class, - PropertySources.class)) { + PropertySources.class, true)) { if (this.propertySourceRegistry != null) { this.propertySourceRegistry.processPropertySource(propertySource); } 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 e77b6fe0b6..1157a9b9d5 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 @@ -231,6 +231,8 @@ class PropertySourceAnnotationTests { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(MultipleComposedAnnotationsConfig.class); ctx.getBean(MultipleComposedAnnotationsConfig.class); assertEnvironmentContainsProperties(ctx, "from.p1", "from.p2", "from.p3", "from.p4", "from.p5"); + // p5 should 'win' as it is registered via the last "locally declared" direct annotation + assertEnvironmentProperty(ctx, "testbean.name", "p5TestBean"); ctx.close(); } 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 b0d3844bc6..f82743ab1d 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 @@ -17,6 +17,7 @@ package org.springframework.core.type; import java.lang.annotation.Annotation; +import java.util.Comparator; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -179,14 +180,56 @@ public interface AnnotatedTypeMetadata { * @return the set of all merged repeatable {@code AnnotationAttributes} found, * or an empty set if none were found * @since 6.1 + * @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean, boolean) */ default Set getMergedRepeatableAnnotationAttributes( Class annotationType, Class containerType, boolean classValuesAsString) { - Adapt[] adaptations = Adapt.values(classValuesAsString, true); - return getAnnotations().stream() - .filter(MergedAnnotationPredicates.typeIn(containerType, annotationType)) + return getMergedRepeatableAnnotationAttributes(annotationType, containerType, classValuesAsString, false); + } + + /** + * 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. + *

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 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 + * @see #getMergedRepeatableAnnotationAttributes(Class, Class, boolean) + */ + default Set getMergedRepeatableAnnotationAttributes( + Class annotationType, Class containerType, + boolean classValuesAsString, boolean sortByReversedMetaDistance) { + + Stream> stream = getAnnotations().stream() + .filter(MergedAnnotationPredicates.typeIn(containerType, annotationType)); + + if (sortByReversedMetaDistance) { + stream = stream.sorted(reversedMetaDistance()); + } + + Adapt[] adaptations = Adapt.values(false, true); + return stream .map(annotation -> annotation.asAnnotationAttributes(adaptations)) .flatMap(attributes -> { if (containerType.equals(attributes.annotationType())) { @@ -197,4 +240,9 @@ public interface AnnotatedTypeMetadata { .collect(Collectors.toCollection(LinkedHashSet::new)); } + + private static Comparator> reversedMetaDistance() { + return Comparator.> comparingInt(MergedAnnotation::getDistance).reversed(); + } + } diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index 57cdba7e70..fef647dae4 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java @@ -263,6 +263,20 @@ class AnnotationMetadataTests { assertRepeatableAnnotations(metadata); } + @Test // gh-31074 + void multipleComposedRepeatableAnnotationsSortedByReversedMetaDistanceUsingStandardAnnotationMetadata() { + AnnotationMetadata metadata = AnnotationMetadata.introspect(MultipleComposedRepeatableAnnotationsClass.class); + assertRepeatableAnnotationsSortedByReversedMetaDistance(metadata); + } + + @Test // gh-31074 + void multipleComposedRepeatableAnnotationsSortedByReversedMetaDistanceUsingSimpleAnnotationMetadata() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(MultipleComposedRepeatableAnnotationsClass.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + assertRepeatableAnnotationsSortedByReversedMetaDistance(metadata); + } + @Test // gh-31041 void multipleRepeatableAnnotationsInContainersUsingStandardAnnotationMetadata() { AnnotationMetadata metadata = AnnotationMetadata.introspect(MultipleRepeatableAnnotationsInContainersClass.class); @@ -318,6 +332,18 @@ class AnnotationMetadataTests { .containsExactly("A", "B", "C", "D"); } + private static void assertRepeatableAnnotationsSortedByReversedMetaDistance(AnnotationMetadata metadata) { + // Note: although the real @ComponentScan annotation is not looked up using + // "sortByReversedMetaDistance" semantics, we can still use @TestComponentScan + // to verify the expected behavior. + Set attributesSet = + metadata.getMergedRepeatableAnnotationAttributes(TestComponentScan.class, TestComponentScans.class, false, true); + assertThat(attributesSet.stream().map(attributes -> attributes.getStringArray("value")).flatMap(Arrays::stream)) + .containsExactly("C", "D", "A", "B"); + assertThat(attributesSet.stream().map(attributes -> attributes.getStringArray("basePackages")).flatMap(Arrays::stream)) + .containsExactly("C", "D", "A", "B"); + } + private static void assertRepeatableAnnotations(Set annotations) { assertThat(annotations.stream().map(TestComponentScan::value).flatMap(Arrays::stream)) .containsExactly("A", "B", "C", "D");