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 a9db6d3d599..b0d3844bc6f 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,8 +17,13 @@ package org.springframework.core.type; import java.lang.annotation.Annotation; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation.Adapt; import org.springframework.core.annotation.MergedAnnotationCollectors; @@ -155,4 +160,41 @@ public interface AnnotatedTypeMetadata { map -> (map.isEmpty() ? null : map), adaptations)); } + /** + * 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. + * @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 + * @return the set of all merged repeatable {@code AnnotationAttributes} found, + * or an empty set if none were found + * @since 6.1 + */ + default Set getMergedRepeatableAnnotationAttributes( + Class annotationType, Class containerType, + boolean classValuesAsString) { + + Adapt[] adaptations = Adapt.values(classValuesAsString, true); + return getAnnotations().stream() + .filter(MergedAnnotationPredicates.typeIn(containerType, annotationType)) + .map(annotation -> annotation.asAnnotationAttributes(adaptations)) + .flatMap(attributes -> { + if (containerType.equals(attributes.annotationType())) { + return Stream.of(attributes.getAnnotationArray(MergedAnnotation.VALUE)); + } + return Stream.of(attributes); + }) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + } 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 082b7ecb4da..57cdba7e70c 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 @@ -21,6 +21,7 @@ import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -32,6 +33,7 @@ import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.testfixture.stereotype.Component; import org.springframework.core.type.classreading.MetadataReader; @@ -247,6 +249,82 @@ class AnnotationMetadataTests { assertMultipleAnnotationsWithIdenticalAttributeNames(metadata); } + @Test // gh-31041 + void multipleComposedRepeatableAnnotationsUsingStandardAnnotationMetadata() { + AnnotationMetadata metadata = AnnotationMetadata.introspect(MultipleComposedRepeatableAnnotationsClass.class); + assertRepeatableAnnotations(metadata); + } + + @Test // gh-31041 + void multipleComposedRepeatableAnnotationsUsingSimpleAnnotationMetadata() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(MultipleComposedRepeatableAnnotationsClass.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + assertRepeatableAnnotations(metadata); + } + + @Test // gh-31041 + void multipleRepeatableAnnotationsInContainersUsingStandardAnnotationMetadata() { + AnnotationMetadata metadata = AnnotationMetadata.introspect(MultipleRepeatableAnnotationsInContainersClass.class); + assertRepeatableAnnotations(metadata); + } + + @Test // gh-31041 + void multipleRepeatableAnnotationsInContainersUsingSimpleAnnotationMetadata() throws Exception { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(MultipleRepeatableAnnotationsInContainersClass.class.getName()); + AnnotationMetadata metadata = metadataReader.getAnnotationMetadata(); + assertRepeatableAnnotations(metadata); + } + + /** + * Tests {@code AnnotatedElementUtils#getMergedRepeatableAnnotations()} variants to ensure that + * {@link AnnotationMetadata#getMergedRepeatableAnnotationAttributes(Class, Class, boolean)} + * behaves the same. + */ + @Test // gh-31041 + void multipleComposedRepeatableAnnotationsUsingAnnotatedElementUtils() throws Exception { + Class element = MultipleComposedRepeatableAnnotationsClass.class; + + Set annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(element, TestComponentScan.class); + assertRepeatableAnnotations(annotations); + + annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(element, TestComponentScan.class, TestComponentScans.class); + assertRepeatableAnnotations(annotations); + } + + /** + * Tests {@code AnnotatedElementUtils#getMergedRepeatableAnnotations()} variants to ensure that + * {@link AnnotationMetadata#getMergedRepeatableAnnotationAttributes(Class, Class, boolean)} + * behaves the same. + */ + @Test // gh-31041 + void multipleRepeatableAnnotationsInContainersUsingAnnotatedElementUtils() throws Exception { + Class element = MultipleRepeatableAnnotationsInContainersClass.class; + + Set annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(element, TestComponentScan.class); + assertRepeatableAnnotations(annotations); + + annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(element, TestComponentScan.class, TestComponentScans.class); + assertRepeatableAnnotations(annotations); + } + + private static void assertRepeatableAnnotations(AnnotationMetadata metadata) { + Set attributesSet = + metadata.getMergedRepeatableAnnotationAttributes(TestComponentScan.class, TestComponentScans.class, false); + assertThat(attributesSet.stream().map(attributes -> attributes.getStringArray("value")).flatMap(Arrays::stream)) + .containsExactly("A", "B", "C", "D"); + assertThat(attributesSet.stream().map(attributes -> attributes.getStringArray("basePackages")).flatMap(Arrays::stream)) + .containsExactly("A", "B", "C", "D"); + } + + private static void assertRepeatableAnnotations(Set annotations) { + assertThat(annotations.stream().map(TestComponentScan::value).flatMap(Arrays::stream)) + .containsExactly("A", "B", "C", "D"); + assertThat(annotations.stream().map(TestComponentScan::basePackages).flatMap(Arrays::stream)) + .containsExactly("A", "B", "C", "D"); + } + @Test void inheritedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingStandardAnnotationMetadata() { AnnotationMetadata metadata = AnnotationMetadata.introspect(NamedComposedAnnotationExtended.class); @@ -534,6 +612,14 @@ class AnnotationMetadataTests { @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) + public @interface TestComponentScans { + + TestComponentScan[] value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @Repeatable(TestComponentScans.class) public @interface TestComponentScan { @AliasFor("basePackages") @@ -560,6 +646,40 @@ class AnnotationMetadataTests { public static class ComposedConfigurationWithAttributeOverridesClass { } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @TestComponentScan("C") + public @interface ScanPackageC { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @TestComponentScan("D") + public @interface ScanPackageD { + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + @TestComponentScans({ + @TestComponentScan("C"), + @TestComponentScan("D") + }) + public @interface ScanPackagesCandD { + } + + @TestComponentScan("A") + @ScanPackageC + @ScanPackageD + @TestComponentScan("B") + static class MultipleComposedRepeatableAnnotationsClass { + } + + @TestComponentScan("A") + @ScanPackagesCandD + @TestComponentScans(@TestComponentScan("B")) + static class MultipleRepeatableAnnotationsInContainersClass { + } + @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface NamedAnnotation1 {