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
This commit is contained in:
@@ -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<TestComponentScan> 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<TestComponentScan> 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<AnnotationAttributes> 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<TestComponentScan> 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 {
|
||||
|
||||
Reference in New Issue
Block a user