Skip to content

Commit 65ab67e

Browse files
committed
Merge branch '5.3.x'
2 parents 6c3a719 + 828f74f commit 65ab67e

File tree

3 files changed

+214
-6
lines changed

3 files changed

+214
-6
lines changed

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

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

Lines changed: 10 additions & 5 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.
@@ -39,6 +39,7 @@
3939
* <p>To completely disable repeatable support use {@link #none()}.
4040
*
4141
* @author Phillip Webb
42+
* @author Sam Brannen
4243
* @since 5.2
4344
*/
4445
public abstract class RepeatableContainers {
@@ -101,15 +102,19 @@ public static RepeatableContainers standardRepeatables() {
101102
}
102103

103104
/**
104-
* Create a {@link RepeatableContainers} instance that uses a defined
105-
* container and repeatable type.
106-
* @param repeatable the contained repeatable annotation
107-
* @param container the container annotation or {@code null}. If specified,
105+
* Create a {@link RepeatableContainers} instance that uses predefined
106+
* repeatable and container types.
107+
* @param repeatable the contained repeatable annotation type
108+
* @param container the container annotation type or {@code null}. If specified,
108109
* this annotation must declare a {@code value} attribute returning an array
109110
* of repeatable annotations. If not specified, the container will be
110111
* deduced by inspecting the {@code @Repeatable} annotation on
111112
* {@code repeatable}.
112113
* @return a {@link RepeatableContainers} instance
114+
* @throws IllegalArgumentException if the supplied container type is
115+
* {@code null} and the annotation type is not a repeatable annotation
116+
* @throws AnnotationConfigurationException if the supplied container type
117+
* is not a properly configured container for a repeatable annotation
113118
*/
114119
public static RepeatableContainers of(
115120
Class<? extends Annotation> repeatable, @Nullable Class<? extends Annotation> container) {
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)