Skip to content

Commit 828f74f

Browse files
committed
Support nesting in AnnotatedElementUtils.findMergedRepeatableAnnotations()
Prior to this commit, the findMergedRepeatableAnnotations() methods in AnnotatedElementUtils failed to find repeatable annotations declared on other repeatable annotations (i.e., when one repeatable annotation type was used as a meta-annotation on a different repeatable annotation type). The reason is that findMergedRepeatableAnnotations(element, annotationType, containerType) always used RepeatableContainers.of(annotationType, containerType) to create a RepeatableContainers instance, even if the supplied containerType was null. Doing so restricts the search to supporting only repeatable annotations whose container is the supplied containerType and prevents the search from finding repeatable annotations declared as meta-annotations on other types of repeatable annotations. Note, however, that direct use of the MergedAnnotations API already supported finding nested repeatable annotations when using RepeatableContainers.standardRepeatables() or RepeatableContainers.of(...).and(...).and(...). The latter composes support for multiple repeatable annotation types and their containers. This commit addresses the issue for findMergedRepeatableAnnotations() when the containerType is null or not provided. However, findMergedRepeatableAnnotations(element, annotationType, containerType) still suffers from the aforementioned limitation, and the Javadoc has been updated to make that clear. Closes gh-20279
1 parent 965dd66 commit 828f74f

File tree

2 files changed

+205
-2
lines changed

2 files changed

+205
-2
lines changed

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

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2022 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.
@@ -725,6 +725,16 @@ public static <A extends Annotation> Set<A> findMergedRepeatableAnnotations(Anno
725725
* single annotation and within annotation hierarchies.
726726
* <p>This method follows <em>find semantics</em> as described in the
727727
* {@linkplain AnnotatedElementUtils class-level javadoc}.
728+
* <p><strong>WARNING</strong>: if the supplied {@code containerType} is not
729+
* {@code null}, the search will be restricted to supporting only repeatable
730+
* annotations whose container is the supplied {@code containerType}. This
731+
* prevents the search from finding repeatable annotations declared as
732+
* meta-annotations on other types of repeatable annotations. If you need to
733+
* support such a use case, favor {@link #findMergedRepeatableAnnotations(AnnotatedElement, Class)}
734+
* over this method or alternatively use the {@link MergedAnnotations} API
735+
* directly in conjunction with {@link RepeatableContainers} that are
736+
* {@linkplain RepeatableContainers#and(Class, Class) composed} to support
737+
* multiple repeatable annotation types.
728738
* @param element the annotated element (never {@code null})
729739
* @param annotationType the annotation type to find (never {@code null})
730740
* @param containerType the type of the container that holds the annotations;
@@ -767,7 +777,23 @@ private static MergedAnnotations findAnnotations(AnnotatedElement element) {
767777
private static MergedAnnotations findRepeatableAnnotations(AnnotatedElement element,
768778
@Nullable Class<? extends Annotation> containerType, Class<? extends Annotation> annotationType) {
769779

770-
RepeatableContainers repeatableContainers = RepeatableContainers.of(annotationType, containerType);
780+
RepeatableContainers repeatableContainers;
781+
if (containerType == null) {
782+
// Invoke RepeatableContainers.of() in order to adhere to the contract of
783+
// findMergedRepeatableAnnotations() which states that an IllegalArgumentException
784+
// will be thrown if the the container cannot be resolved.
785+
//
786+
// In any case, we use standardRepeatables() in order to support repeatable
787+
// annotations on other types of repeatable annotations (i.e., nested repeatable
788+
// annotation types).
789+
//
790+
// See https://github.com/spring-projects/spring-framework/issues/20279
791+
RepeatableContainers.of(annotationType, null);
792+
repeatableContainers = RepeatableContainers.standardRepeatables();
793+
}
794+
else {
795+
repeatableContainers = RepeatableContainers.of(annotationType, containerType);
796+
}
771797
return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY, repeatableContainers);
772798
}
773799

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.core.annotation;
18+
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Repeatable;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
import java.lang.reflect.Method;
25+
import java.util.Set;
26+
27+
import org.junit.jupiter.api.Nested;
28+
import org.junit.jupiter.api.Test;
29+
30+
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
31+
import org.springframework.util.ReflectionUtils;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
35+
/**
36+
* Tests for various ways to search for repeatable annotations that are
37+
* nested (i.e., repeatable annotations used as meta-annotations on other
38+
* repeatable annotations).
39+
*
40+
* @author Sam Brannen
41+
* @since 5.3.24
42+
* @see https://github.com/spring-projects/spring-framework/issues/20279
43+
*/
44+
@SuppressWarnings("unused")
45+
class NestedRepeatableAnnotationsTests {
46+
47+
@Nested
48+
class SingleRepeatableAnnotationTests {
49+
50+
private final Method method = ReflectionUtils.findMethod(getClass(), "annotatedMethod");
51+
52+
@Test
53+
void streamRepeatableAnnotations_MergedAnnotationsApi() {
54+
Set<A> annotations = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY)
55+
.stream(A.class).collect(MergedAnnotationCollectors.toAnnotationSet());
56+
// Merged, so we expect to find @A once with its value coming from @B(5).
57+
assertThat(annotations).extracting(A::value).containsExactly(5);
58+
}
59+
60+
@Test
61+
void findMergedRepeatableAnnotations_AnnotatedElementUtils() {
62+
Set<A> annotations = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, A.class);
63+
// Merged, so we expect to find @A once with its value coming from @B(5).
64+
assertThat(annotations).extracting(A::value).containsExactly(5);
65+
}
66+
67+
@Test
68+
@SuppressWarnings("deprecation")
69+
void getRepeatableAnnotations_AnnotationUtils() {
70+
Set<A> annotations = AnnotationUtils.getRepeatableAnnotations(method, A.class);
71+
// Not merged, so we expect to find @A once with the default value of 0.
72+
// @A will actually be found twice, but we have Set semantics here.
73+
assertThat(annotations).extracting(A::value).containsExactly(0);
74+
}
75+
76+
@B(5)
77+
void annotatedMethod() {
78+
}
79+
80+
}
81+
82+
@Nested
83+
class MultipleRepeatableAnnotationsTests {
84+
85+
private final Method method = ReflectionUtils.findMethod(getClass(), "annotatedMethod");
86+
87+
@Test
88+
void streamRepeatableAnnotationsWithStandardRepeatables_MergedAnnotationsApi() {
89+
RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables();
90+
Set<A> annotations = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY, repeatableContainers)
91+
.stream(A.class).collect(MergedAnnotationCollectors.toAnnotationSet());
92+
// Merged, so we expect to find @A twice with values coming from @B(5) and @B(10).
93+
assertThat(annotations).extracting(A::value).containsExactly(5, 10);
94+
}
95+
96+
@Test
97+
void streamRepeatableAnnotationsWithExplicitRepeatables_MergedAnnotationsApi() {
98+
RepeatableContainers repeatableContainers =
99+
RepeatableContainers.of(A.class, A.Container.class).and(B.Container.class, B.class);
100+
Set<A> annotations = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY, repeatableContainers)
101+
.stream(A.class).collect(MergedAnnotationCollectors.toAnnotationSet());
102+
// Merged, so we expect to find @A twice with values coming from @B(5) and @B(10).
103+
assertThat(annotations).extracting(A::value).containsExactly(5, 10);
104+
}
105+
106+
@Test
107+
void findMergedRepeatableAnnotationsWithStandardRepeatables_AnnotatedElementUtils() {
108+
Set<A> annotations = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, A.class);
109+
// Merged, so we expect to find @A twice with values coming from @B(5) and @B(10).
110+
// However, findMergedRepeatableAnnotations() currently finds ZERO annotations.
111+
assertThat(annotations).extracting(A::value).containsExactly(5, 10);
112+
}
113+
114+
@Test
115+
void findMergedRepeatableAnnotationsWithExplicitContainer_AnnotatedElementUtils() {
116+
Set<A> annotations = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, A.class, A.Container.class);
117+
// When findMergedRepeatableAnnotations(...) is invoked with an explicit container
118+
// type, it uses RepeatableContainers.of(...) which limits the repeatable annotation
119+
// support to a single container type.
120+
//
121+
// In this test case, we are therefore limiting the support to @A.Container, which
122+
// means that @B.Container is unsupported and effectively ignored as a repeatable
123+
// container type.
124+
//
125+
// Long story, short: the search doesn't find anything.
126+
assertThat(annotations).isEmpty();
127+
}
128+
129+
@Test
130+
@SuppressWarnings("deprecation")
131+
void getRepeatableAnnotations_AnnotationUtils() {
132+
Set<A> annotations = AnnotationUtils.getRepeatableAnnotations(method, A.class);
133+
// Not merged, so we expect to find a single @A with default value of 0.
134+
// @A will actually be found twice, but we have Set semantics here.
135+
assertThat(annotations).extracting(A::value).containsExactly(0);
136+
}
137+
138+
@B(5)
139+
@B(10)
140+
void annotatedMethod() {
141+
}
142+
143+
}
144+
145+
146+
@Retention(RetentionPolicy.RUNTIME)
147+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
148+
@Repeatable(A.Container.class)
149+
public @interface A {
150+
151+
int value() default 0;
152+
153+
@Retention(RetentionPolicy.RUNTIME)
154+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
155+
@interface Container {
156+
A[] value();
157+
}
158+
}
159+
160+
@Retention(RetentionPolicy.RUNTIME)
161+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
162+
@Repeatable(B.Container.class)
163+
@A
164+
@A
165+
public @interface B {
166+
167+
@AliasFor(annotation = A.class)
168+
int value();
169+
170+
@Retention(RetentionPolicy.RUNTIME)
171+
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
172+
@interface Container {
173+
B[] value();
174+
}
175+
}
176+
177+
}

0 commit comments

Comments
 (0)