Browse Source

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
pull/31063/head
Sam Brannen 1 year ago
parent
commit
0b902f32f6
  1. 42
      spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java
  2. 120
      spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java

42
spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java

@ -17,8 +17,13 @@ @@ -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 { @@ -155,4 +160,41 @@ public interface AnnotatedTypeMetadata {
map -> (map.isEmpty() ? null : map), adaptations));
}
/**
* Retrieve all <em>repeatable annotations</em> of the given type within the
* annotation hierarchy <em>above</em> the underlying element (as direct
* annotation or meta-annotation); and for each annotation found, merge that
* annotation's attributes with <em>matching</em> attributes from annotations
* in lower levels of the annotation hierarchy and store the results in an
* instance of {@link AnnotationAttributes}.
* <p>{@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<AnnotationAttributes> getMergedRepeatableAnnotationAttributes(
Class<? extends Annotation> annotationType, Class<? extends Annotation> 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));
}
}

120
spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java

@ -21,6 +21,7 @@ import java.lang.annotation.Annotation; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 {

Loading…
Cancel
Save