Skip to content

Commit 0b902f3

Browse files
committed
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
1 parent fb6c325 commit 0b902f3

File tree

2 files changed

+162
-0
lines changed

2 files changed

+162
-0
lines changed

Diff for: spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java

+42
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@
1717
package org.springframework.core.type;
1818

1919
import java.lang.annotation.Annotation;
20+
import java.util.LinkedHashSet;
2021
import java.util.Map;
22+
import java.util.Set;
23+
import java.util.stream.Collectors;
24+
import java.util.stream.Stream;
2125

26+
import org.springframework.core.annotation.AnnotationAttributes;
2227
import org.springframework.core.annotation.MergedAnnotation;
2328
import org.springframework.core.annotation.MergedAnnotation.Adapt;
2429
import org.springframework.core.annotation.MergedAnnotationCollectors;
@@ -155,4 +160,41 @@ default MultiValueMap<String, Object> getAllAnnotationAttributes(
155160
map -> (map.isEmpty() ? null : map), adaptations));
156161
}
157162

163+
/**
164+
* Retrieve all <em>repeatable annotations</em> of the given type within the
165+
* annotation hierarchy <em>above</em> the underlying element (as direct
166+
* annotation or meta-annotation); and for each annotation found, merge that
167+
* annotation's attributes with <em>matching</em> attributes from annotations
168+
* in lower levels of the annotation hierarchy and store the results in an
169+
* instance of {@link AnnotationAttributes}.
170+
* <p>{@link org.springframework.core.annotation.AliasFor @AliasFor} semantics
171+
* are fully supported, both within a single annotation and within annotation
172+
* hierarchies.
173+
* @param annotationType the annotation type to find
174+
* @param containerType the type of the container that holds the annotations
175+
* @param classValuesAsString whether to convert class references to {@code String}
176+
* class names for exposure as values in the returned {@code AnnotationAttributes},
177+
* instead of {@code Class} references which might potentially have to be loaded
178+
* first
179+
* @return the set of all merged repeatable {@code AnnotationAttributes} found,
180+
* or an empty set if none were found
181+
* @since 6.1
182+
*/
183+
default Set<AnnotationAttributes> getMergedRepeatableAnnotationAttributes(
184+
Class<? extends Annotation> annotationType, Class<? extends Annotation> containerType,
185+
boolean classValuesAsString) {
186+
187+
Adapt[] adaptations = Adapt.values(classValuesAsString, true);
188+
return getAnnotations().stream()
189+
.filter(MergedAnnotationPredicates.typeIn(containerType, annotationType))
190+
.map(annotation -> annotation.asAnnotationAttributes(adaptations))
191+
.flatMap(attributes -> {
192+
if (containerType.equals(attributes.annotationType())) {
193+
return Stream.of(attributes.getAnnotationArray(MergedAnnotation.VALUE));
194+
}
195+
return Stream.of(attributes);
196+
})
197+
.collect(Collectors.toCollection(LinkedHashSet::new));
198+
}
199+
158200
}

Diff for: spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java

+120
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.lang.annotation.Documented;
2222
import java.lang.annotation.ElementType;
2323
import java.lang.annotation.Inherited;
24+
import java.lang.annotation.Repeatable;
2425
import java.lang.annotation.Retention;
2526
import java.lang.annotation.RetentionPolicy;
2627
import java.lang.annotation.Target;
@@ -32,6 +33,7 @@
3233
import org.junit.jupiter.api.Test;
3334

3435
import org.springframework.core.annotation.AliasFor;
36+
import org.springframework.core.annotation.AnnotatedElementUtils;
3537
import org.springframework.core.annotation.AnnotationAttributes;
3638
import org.springframework.core.testfixture.stereotype.Component;
3739
import org.springframework.core.type.classreading.MetadataReader;
@@ -247,6 +249,82 @@ void composedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingSimple
247249
assertMultipleAnnotationsWithIdenticalAttributeNames(metadata);
248250
}
249251

252+
@Test // gh-31041
253+
void multipleComposedRepeatableAnnotationsUsingStandardAnnotationMetadata() {
254+
AnnotationMetadata metadata = AnnotationMetadata.introspect(MultipleComposedRepeatableAnnotationsClass.class);
255+
assertRepeatableAnnotations(metadata);
256+
}
257+
258+
@Test // gh-31041
259+
void multipleComposedRepeatableAnnotationsUsingSimpleAnnotationMetadata() throws Exception {
260+
MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
261+
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(MultipleComposedRepeatableAnnotationsClass.class.getName());
262+
AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
263+
assertRepeatableAnnotations(metadata);
264+
}
265+
266+
@Test // gh-31041
267+
void multipleRepeatableAnnotationsInContainersUsingStandardAnnotationMetadata() {
268+
AnnotationMetadata metadata = AnnotationMetadata.introspect(MultipleRepeatableAnnotationsInContainersClass.class);
269+
assertRepeatableAnnotations(metadata);
270+
}
271+
272+
@Test // gh-31041
273+
void multipleRepeatableAnnotationsInContainersUsingSimpleAnnotationMetadata() throws Exception {
274+
MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
275+
MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(MultipleRepeatableAnnotationsInContainersClass.class.getName());
276+
AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
277+
assertRepeatableAnnotations(metadata);
278+
}
279+
280+
/**
281+
* Tests {@code AnnotatedElementUtils#getMergedRepeatableAnnotations()} variants to ensure that
282+
* {@link AnnotationMetadata#getMergedRepeatableAnnotationAttributes(Class, Class, boolean)}
283+
* behaves the same.
284+
*/
285+
@Test // gh-31041
286+
void multipleComposedRepeatableAnnotationsUsingAnnotatedElementUtils() throws Exception {
287+
Class<?> element = MultipleComposedRepeatableAnnotationsClass.class;
288+
289+
Set<TestComponentScan> annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(element, TestComponentScan.class);
290+
assertRepeatableAnnotations(annotations);
291+
292+
annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(element, TestComponentScan.class, TestComponentScans.class);
293+
assertRepeatableAnnotations(annotations);
294+
}
295+
296+
/**
297+
* Tests {@code AnnotatedElementUtils#getMergedRepeatableAnnotations()} variants to ensure that
298+
* {@link AnnotationMetadata#getMergedRepeatableAnnotationAttributes(Class, Class, boolean)}
299+
* behaves the same.
300+
*/
301+
@Test // gh-31041
302+
void multipleRepeatableAnnotationsInContainersUsingAnnotatedElementUtils() throws Exception {
303+
Class<?> element = MultipleRepeatableAnnotationsInContainersClass.class;
304+
305+
Set<TestComponentScan> annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(element, TestComponentScan.class);
306+
assertRepeatableAnnotations(annotations);
307+
308+
annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(element, TestComponentScan.class, TestComponentScans.class);
309+
assertRepeatableAnnotations(annotations);
310+
}
311+
312+
private static void assertRepeatableAnnotations(AnnotationMetadata metadata) {
313+
Set<AnnotationAttributes> attributesSet =
314+
metadata.getMergedRepeatableAnnotationAttributes(TestComponentScan.class, TestComponentScans.class, false);
315+
assertThat(attributesSet.stream().map(attributes -> attributes.getStringArray("value")).flatMap(Arrays::stream))
316+
.containsExactly("A", "B", "C", "D");
317+
assertThat(attributesSet.stream().map(attributes -> attributes.getStringArray("basePackages")).flatMap(Arrays::stream))
318+
.containsExactly("A", "B", "C", "D");
319+
}
320+
321+
private static void assertRepeatableAnnotations(Set<TestComponentScan> annotations) {
322+
assertThat(annotations.stream().map(TestComponentScan::value).flatMap(Arrays::stream))
323+
.containsExactly("A", "B", "C", "D");
324+
assertThat(annotations.stream().map(TestComponentScan::basePackages).flatMap(Arrays::stream))
325+
.containsExactly("A", "B", "C", "D");
326+
}
327+
250328
@Test
251329
void inheritedAnnotationWithMetaAnnotationsWithIdenticalAttributeNamesUsingStandardAnnotationMetadata() {
252330
AnnotationMetadata metadata = AnnotationMetadata.introspect(NamedComposedAnnotationExtended.class);
@@ -534,6 +612,14 @@ private static class AnnotatedComponentSubClass extends AnnotatedComponent {
534612

535613
@Retention(RetentionPolicy.RUNTIME)
536614
@Target(ElementType.TYPE)
615+
public @interface TestComponentScans {
616+
617+
TestComponentScan[] value();
618+
}
619+
620+
@Retention(RetentionPolicy.RUNTIME)
621+
@Target(ElementType.TYPE)
622+
@Repeatable(TestComponentScans.class)
537623
public @interface TestComponentScan {
538624

539625
@AliasFor("basePackages")
@@ -560,6 +646,40 @@ private static class AnnotatedComponentSubClass extends AnnotatedComponent {
560646
public static class ComposedConfigurationWithAttributeOverridesClass {
561647
}
562648

649+
@Retention(RetentionPolicy.RUNTIME)
650+
@Target(ElementType.TYPE)
651+
@TestComponentScan("C")
652+
public @interface ScanPackageC {
653+
}
654+
655+
@Retention(RetentionPolicy.RUNTIME)
656+
@Target(ElementType.TYPE)
657+
@TestComponentScan("D")
658+
public @interface ScanPackageD {
659+
}
660+
661+
@Retention(RetentionPolicy.RUNTIME)
662+
@Target(ElementType.TYPE)
663+
@TestComponentScans({
664+
@TestComponentScan("C"),
665+
@TestComponentScan("D")
666+
})
667+
public @interface ScanPackagesCandD {
668+
}
669+
670+
@TestComponentScan("A")
671+
@ScanPackageC
672+
@ScanPackageD
673+
@TestComponentScan("B")
674+
static class MultipleComposedRepeatableAnnotationsClass {
675+
}
676+
677+
@TestComponentScan("A")
678+
@ScanPackagesCandD
679+
@TestComponentScans(@TestComponentScan("B"))
680+
static class MultipleRepeatableAnnotationsInContainersClass {
681+
}
682+
563683
@Retention(RetentionPolicy.RUNTIME)
564684
@Target(ElementType.TYPE)
565685
public @interface NamedAnnotation1 {

0 commit comments

Comments
 (0)