Skip to content

Commit f5baa32

Browse files
committed
Include repeatable annotation container in MergedAnnotations results
A bug has existed in Spring's MergedAnnotations support since it was introduced in Spring Framework 5.2. Specifically, if the MergedAnnotations API is used to search for annotations with "standard repeatable annotation" support enabled (which is the default), it's possible to search for a repeatable annotation but not for the repeatable annotation's container annotation. The reason is that MergedAnnotationFinder.process(Object, int, Object, Annotation) does not process the container annotation and instead only processes the "contained" annotations, which prevents a container annotation from being included in search results. In #29685, we fixed a bug that prevented the MergedAnnotations support from recognizing an annotation as a container if the container annotation declares attributes other than the required `value` attribute. As a consequence of that bug fix, since Spring Framework 5.3.25, the MergedAnnotations infrastructure considers such an annotation a container, and due to the aforementioned bug the container is no longer processed, which results in a regression in behavior for annotation searches for such a container annotation. This commit addresses the original bug as well as the regression by processing container annotations in addition to the contained repeatable annotations. See gh-29685 Closes gh-32731 (cherry picked from commit 4baad16)
1 parent 924b684 commit f5baa32

File tree

2 files changed

+68
-3
lines changed

2 files changed

+68
-3
lines changed

spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -410,7 +410,10 @@ private MergedAnnotation<A> process(
410410

411411
Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations(annotation);
412412
if (repeatedAnnotations != null) {
413-
return doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations);
413+
MergedAnnotation<A> result = doWithAnnotations(type, aggregateIndex, source, repeatedAnnotations);
414+
if (result != null) {
415+
return result;
416+
}
414417
}
415418
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(
416419
annotation.annotationType(), repeatableContainers, annotationFilter);

spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java

+63-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.lang.annotation.RetentionPolicy;
2525
import java.lang.annotation.Target;
2626
import java.lang.reflect.AnnotatedElement;
27+
import java.util.Arrays;
2728
import java.util.Set;
2829
import java.util.stream.Stream;
2930

@@ -175,7 +176,7 @@ void typeHierarchyWhenOnClassReturnsAnnotations() {
175176
}
176177

177178
@Test
178-
void typeHierarchyWhenWhenOnSuperclassReturnsAnnotations() {
179+
void typeHierarchyWhenOnSuperclassReturnsAnnotations() {
179180
Set<PeteRepeat> annotations = getAnnotations(null, PeteRepeat.class,
180181
SearchStrategy.TYPE_HIERARCHY, SubRepeatableClass.class);
181182
assertThat(annotations.stream().map(PeteRepeat::value)).containsExactly("A", "B",
@@ -240,6 +241,44 @@ void typeHierarchyAnnotationsWithLocalComposedAnnotationWhoseRepeatableMetaAnnot
240241
assertThat(annotationTypes).containsExactly(WithRepeatedMetaAnnotations.class, Noninherited.class, Noninherited.class);
241242
}
242243

244+
@Test // gh-32731
245+
void searchFindsRepeatableContainerAnnotationAndRepeatedAnnotations() {
246+
Class<?> clazz = StandardRepeatablesWithContainerWithMultipleAttributesTestCase.class;
247+
248+
// NO RepeatableContainers
249+
MergedAnnotations mergedAnnotations = MergedAnnotations.from(clazz, TYPE_HIERARCHY, RepeatableContainers.none());
250+
ContainerWithMultipleAttributes container = mergedAnnotations
251+
.get(ContainerWithMultipleAttributes.class)
252+
.synthesize(MergedAnnotation::isPresent).orElse(null);
253+
assertThat(container).as("container").isNotNull();
254+
assertThat(container.name()).isEqualTo("enigma");
255+
RepeatableWithContainerWithMultipleAttributes[] repeatedAnnotations = container.value();
256+
assertThat(Arrays.stream(repeatedAnnotations).map(RepeatableWithContainerWithMultipleAttributes::value))
257+
.containsExactly("A", "B");
258+
Set<RepeatableWithContainerWithMultipleAttributes> set =
259+
mergedAnnotations.stream(RepeatableWithContainerWithMultipleAttributes.class)
260+
.collect(MergedAnnotationCollectors.toAnnotationSet());
261+
// Only finds the locally declared repeated annotation.
262+
assertThat(set.stream().map(RepeatableWithContainerWithMultipleAttributes::value))
263+
.containsExactly("C");
264+
265+
// Standard RepeatableContainers
266+
mergedAnnotations = MergedAnnotations.from(clazz, TYPE_HIERARCHY, RepeatableContainers.standardRepeatables());
267+
container = mergedAnnotations
268+
.get(ContainerWithMultipleAttributes.class)
269+
.synthesize(MergedAnnotation::isPresent).orElse(null);
270+
assertThat(container).as("container").isNotNull();
271+
assertThat(container.name()).isEqualTo("enigma");
272+
repeatedAnnotations = container.value();
273+
assertThat(Arrays.stream(repeatedAnnotations).map(RepeatableWithContainerWithMultipleAttributes::value))
274+
.containsExactly("A", "B");
275+
set = mergedAnnotations.stream(RepeatableWithContainerWithMultipleAttributes.class)
276+
.collect(MergedAnnotationCollectors.toAnnotationSet());
277+
// Finds the locally declared repeated annotation plus the 2 in the container.
278+
assertThat(set.stream().map(RepeatableWithContainerWithMultipleAttributes::value))
279+
.containsExactly("A", "B", "C");
280+
}
281+
243282
private <A extends Annotation> Set<A> getAnnotations(Class<? extends Annotation> container,
244283
Class<A> repeatable, SearchStrategy searchStrategy, AnnotatedElement element) {
245284

@@ -449,4 +488,27 @@ static class SubNoninheritedRepeatableClass extends NoninheritedRepeatableClass
449488
static class WithRepeatedMetaAnnotationsClass {
450489
}
451490

491+
@Retention(RetentionPolicy.RUNTIME)
492+
@interface ContainerWithMultipleAttributes {
493+
494+
RepeatableWithContainerWithMultipleAttributes[] value();
495+
496+
String name() default "";
497+
}
498+
499+
@Retention(RetentionPolicy.RUNTIME)
500+
@Repeatable(ContainerWithMultipleAttributes.class)
501+
@interface RepeatableWithContainerWithMultipleAttributes {
502+
503+
String value() default "";
504+
}
505+
506+
@ContainerWithMultipleAttributes(name = "enigma", value = {
507+
@RepeatableWithContainerWithMultipleAttributes("A"),
508+
@RepeatableWithContainerWithMultipleAttributes("B")
509+
})
510+
@RepeatableWithContainerWithMultipleAttributes("C")
511+
static class StandardRepeatablesWithContainerWithMultipleAttributesTestCase {
512+
}
513+
452514
}

0 commit comments

Comments
 (0)