Skip to content

Commit 5ddc984

Browse files
committed
Support repeatable annotation containers with multiple attributes
Prior to this commit, there was a bug in the implementation of StandardRepeatableContainers.computeRepeatedAnnotationsMethod() which has existed since Spring Framework 5.2 (when StandardRepeatableContainers was introduced). Specifically, StandardRepeatableContainers ignored any repeatable container annotation if it declared attributes other than `value()`. However, Java permits any number of attributes in a repeatable container annotation. In addition, the changes made in conjunction with gh-20279 made the bug in StandardRepeatableContainers apparent when using the getMergedRepeatableAnnotations() or findMergedRepeatableAnnotations() method in AnnotatedElementUtils, resulting in regressions for the behavior of those two methods. This commit fixes the regressions and bug by altering the logic in StandardRepeatableContainers.computeRepeatedAnnotationsMethod() so that it explicitly looks for the `value()` method and ignores any other methods declared in a repeatable container annotation candidate. See gh-29685 Closes gh-29686
1 parent b2ce54e commit 5ddc984

File tree

3 files changed

+78
-2
lines changed

3 files changed

+78
-2
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,8 @@ private static Method getRepeatedAnnotationsMethod(Class<? extends Annotation> a
166166

167167
private static Object computeRepeatedAnnotationsMethod(Class<? extends Annotation> annotationType) {
168168
AttributeMethods methods = AttributeMethods.forAnnotationType(annotationType);
169-
if (methods.hasOnlyValueAttribute()) {
170-
Method method = methods.get(0);
169+
Method method = methods.get(MergedAnnotation.VALUE);
170+
if (method != null) {
171171
Class<?> returnType = method.getReturnType();
172172
if (returnType.isArray()) {
173173
Class<?> componentType = returnType.getComponentType();

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.lang.annotation.Documented;
2121
import java.lang.annotation.ElementType;
2222
import java.lang.annotation.Inherited;
23+
import java.lang.annotation.Repeatable;
2324
import java.lang.annotation.Retention;
2425
import java.lang.annotation.RetentionPolicy;
2526
import java.lang.annotation.Target;
@@ -77,6 +78,7 @@
7778
* @see AnnotationUtilsTests
7879
* @see MultipleComposedAnnotationsOnSingleAnnotatedElementTests
7980
* @see ComposedRepeatableAnnotationsTests
81+
* @see NestedRepeatableAnnotationsTests
8082
*/
8183
class AnnotatedElementUtilsTests {
8284

@@ -908,6 +910,31 @@ void getMergedAnnotationOnThreeDeepMetaWithValue() {
908910
assertThat(annotation.value()).containsExactly("FromValueAttributeMeta");
909911
}
910912

913+
/**
914+
* @since 5.3.25
915+
*/
916+
@Test // gh-29685
917+
void getMergedRepeatableAnnotationsWithContainerWithMultipleAttributes() {
918+
Set<StandardRepeatableWithContainerWithMultipleAttributes> repeatableAnnotations =
919+
AnnotatedElementUtils.getMergedRepeatableAnnotations(
920+
StandardRepeatablesWithContainerWithMultipleAttributesTestCase.class,
921+
StandardRepeatableWithContainerWithMultipleAttributes.class);
922+
assertThat(repeatableAnnotations).map(StandardRepeatableWithContainerWithMultipleAttributes::value)
923+
.containsExactly("a", "b");
924+
}
925+
926+
/**
927+
* @since 5.3.25
928+
*/
929+
@Test // gh-29685
930+
void findMergedRepeatableAnnotationsWithContainerWithMultipleAttributes() {
931+
Set<StandardRepeatableWithContainerWithMultipleAttributes> repeatableAnnotations =
932+
AnnotatedElementUtils.findMergedRepeatableAnnotations(
933+
StandardRepeatablesWithContainerWithMultipleAttributesTestCase.class,
934+
StandardRepeatableWithContainerWithMultipleAttributes.class);
935+
assertThat(repeatableAnnotations).map(StandardRepeatableWithContainerWithMultipleAttributes::value)
936+
.containsExactly("a", "b");
937+
}
911938

912939
// -------------------------------------------------------------------------
913940

@@ -1557,4 +1584,24 @@ class ForAnnotationsClass {
15571584
static class ValueAttributeMetaMetaClass {
15581585
}
15591586

1587+
@Retention(RetentionPolicy.RUNTIME)
1588+
@interface StandardContainerWithMultipleAttributes {
1589+
1590+
StandardRepeatableWithContainerWithMultipleAttributes[] value();
1591+
1592+
String name() default "";
1593+
}
1594+
1595+
@Retention(RetentionPolicy.RUNTIME)
1596+
@Repeatable(StandardContainerWithMultipleAttributes.class)
1597+
@interface StandardRepeatableWithContainerWithMultipleAttributes {
1598+
1599+
String value() default "";
1600+
}
1601+
1602+
@StandardRepeatableWithContainerWithMultipleAttributes("a")
1603+
@StandardRepeatableWithContainerWithMultipleAttributes("b")
1604+
static class StandardRepeatablesWithContainerWithMultipleAttributesTestCase {
1605+
}
1606+
15601607
}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,15 @@ void standardRepeatablesWhenContainerReturnsRepeats() {
6767
StandardRepeatablesTestCase.class, StandardContainer.class);
6868
assertThat(values).containsExactly("a", "b");
6969
}
70+
71+
@Test
72+
void standardRepeatablesWithContainerWithMultipleAttributes() {
73+
Object[] values = findRepeatedAnnotationValues(RepeatableContainers.standardRepeatables(),
74+
StandardRepeatablesWithContainerWithMultipleAttributesTestCase.class,
75+
StandardContainerWithMultipleAttributes.class);
76+
assertThat(values).containsExactly("a", "b");
77+
}
78+
7079
}
7180

7281
@Nested
@@ -247,6 +256,26 @@ static class SingleStandardRepeatableTestCase {
247256
static class StandardRepeatablesTestCase {
248257
}
249258

259+
@Retention(RetentionPolicy.RUNTIME)
260+
@interface StandardContainerWithMultipleAttributes {
261+
262+
StandardRepeatableWithContainerWithMultipleAttributes[] value();
263+
264+
String name() default "";
265+
}
266+
267+
@Retention(RetentionPolicy.RUNTIME)
268+
@Repeatable(StandardContainerWithMultipleAttributes.class)
269+
@interface StandardRepeatableWithContainerWithMultipleAttributes {
270+
271+
String value() default "";
272+
}
273+
274+
@StandardRepeatableWithContainerWithMultipleAttributes("a")
275+
@StandardRepeatableWithContainerWithMultipleAttributes("b")
276+
static class StandardRepeatablesWithContainerWithMultipleAttributesTestCase {
277+
}
278+
250279
@ExplicitContainer({ @ExplicitRepeatable("a"), @ExplicitRepeatable("b") })
251280
static class ExplicitRepeatablesTestCase {
252281
}

0 commit comments

Comments
 (0)