From 0b902f32f6a3b2b3579fef9c1c4163d71966f0f8 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 6 Aug 2023 12:02:08 +0300 Subject: [PATCH] Support finding repeatable annotations in AnnotatedTypeMetadata AnnotatedTypeMetadata has various methods for finding annotations; however, prior to this commit it did not provide explicit support for repeatable annotations. Although it is possible to craft a search "query" for repeatable annotations using the MergedAnnotations API via getAnnotations(), that requires intimate knowledge of the MergedAnnotations API as well as the structure of repeatable annotations. Furthermore, the bugs reported in gh-30941 result from the fact that AnnotationConfigUtils attempts to use the existing functionality in AnnotatedTypeMetadata to find repeatable annotations without success. This commit introduces a getMergedRepeatableAnnotationAttributes() method in AnnotatedTypeMetadata that provides dedicated support for finding merged repeatable annotation attributes with full @AliasFor semantics. Closes gh-31041 --- .../core/type/AnnotatedTypeMetadata.java | 42 ++++++ .../core/type/AnnotationMetadataTests.java | 120 ++++++++++++++++++ 2 files changed, 162 insertions(+) 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 a9db6d3d59..b0d3844bc6 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 082b7ecb4d..57cdba7e70 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 {