Skip to content

Commit 274a689

Browse files
committed
Revise RepeatableContainers API to better guide developers
Historically, the Spring Framework first had support for repeatable annotations based on convention and later added explicit support for Java 8's @⁠Repeatable facility. Consequently, the support for both types of repeatable annotations has grown a bit intertwined over the years. However, modern Java applications typically make use of @⁠Repeatable, and convention-based repeatable annotations have become more of a niche. The RepeatableContainers API supports both types of repeatable annotations with @⁠Repeatable support being the default. However, RepeatableContainers.of() makes it very easy to enable support for convention-based repeatable annotations while accidentally disabling support for @⁠Repeatable, which can lead to subtle bugs – for example, if convention-based annotations are combined with @⁠Repeatable annotations. In addition, it is not readily clear how to combine @⁠Repeatable support with convention-based repeatable annotations. In light of the above, this commit revises the RepeatableContainers API to better guide developers to use @⁠Repeatable support for almost all use cases while still supporting convention-based repeatable annotations for special use cases. Specifically: - RepeatableContainers.of() is now deprecated in favor of the new RepeatableContainers.explicitRepeatable() method. - RepeatableContainers.and() is now deprecated in favor of the new RepeatableContainers.plus() method which declares the repeatable and container arguments in the same order as the rest of Spring Framework's repeated annotation APIs. For example, instead of the following confusing mixture of repeatable/container and container/repeatable: RepeatableContainers.of(A.class, A.Container.class) .and(B.Container.class, B.class) Developers are now be able to use: RepeatableContainers.explicitRepeatable(A.class, A.Container.class) .plus(B.class, B.Container.class) This commit also overhauls the Javadoc for RepeatableContainers and explicitly points out that the following is the recommended approach to support convention-based repeatable annotations while retaining support for @⁠Repeatable. RepeatableContainers.standardRepeatables() .plus(MyRepeatable1.class, MyContainer1.class) .plus(MyRepeatable2.class, MyContainer2.class) See gh-20279 Closes gh-34637
1 parent 7d5b389 commit 274a689

File tree

7 files changed

+178
-72
lines changed

7 files changed

+178
-72
lines changed

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

+26-18
Original file line numberDiff line numberDiff line change
@@ -443,13 +443,17 @@ public static <A extends Annotation> Set<A> getMergedRepeatableAnnotations(
443443
* support such a use case, favor {@link #getMergedRepeatableAnnotations(AnnotatedElement, Class)}
444444
* over this method or alternatively use the {@link MergedAnnotations} API
445445
* directly in conjunction with {@link RepeatableContainers} that are
446-
* {@linkplain RepeatableContainers#and(Class, Class) composed} to support
447-
* multiple repeatable annotation types.
446+
* {@linkplain RepeatableContainers#plus(Class, Class) composed} to support
447+
* multiple repeatable annotation types &mdash; for example:
448+
* <pre class="code">
449+
* RepeatableContainers.standardRepeatables()
450+
* .plus(MyRepeatable1.class, MyContainer1.class)
451+
* .plus(MyRepeatable2.class, MyContainer2.class);</pre>
448452
* @param element the annotated element (never {@code null})
449-
* @param annotationType the annotation type to find (never {@code null})
450-
* @param containerType the type of the container that holds the annotations;
451-
* may be {@code null} if the container type should be looked up via
452-
* {@link java.lang.annotation.Repeatable}
453+
* @param annotationType the repeatable annotation type to find (never {@code null})
454+
* @param containerType the type of the container that holds the repeatable
455+
* annotations; may be {@code null} if the container type should be looked up
456+
* via {@link java.lang.annotation.Repeatable @Repeatable}
453457
* @return the set of all merged repeatable {@code Annotations} found,
454458
* or an empty set if none were found
455459
* @throws IllegalArgumentException if the {@code element} or {@code annotationType}
@@ -740,13 +744,17 @@ public static <A extends Annotation> Set<A> findMergedRepeatableAnnotations(Anno
740744
* support such a use case, favor {@link #findMergedRepeatableAnnotations(AnnotatedElement, Class)}
741745
* over this method or alternatively use the {@link MergedAnnotations} API
742746
* directly in conjunction with {@link RepeatableContainers} that are
743-
* {@linkplain RepeatableContainers#and(Class, Class) composed} to support
744-
* multiple repeatable annotation types.
747+
* {@linkplain RepeatableContainers#plus(Class, Class) composed} to support
748+
* multiple repeatable annotation types &mdash; for example:
749+
* <pre class="code">
750+
* RepeatableContainers.standardRepeatables()
751+
* .plus(MyRepeatable1.class, MyContainer1.class)
752+
* .plus(MyRepeatable2.class, MyContainer2.class);</pre>
745753
* @param element the annotated element (never {@code null})
746-
* @param annotationType the annotation type to find (never {@code null})
747-
* @param containerType the type of the container that holds the annotations;
748-
* may be {@code null} if the container type should be looked up via
749-
* {@link java.lang.annotation.Repeatable}
754+
* @param annotationType the repeatable annotation type to find (never {@code null})
755+
* @param containerType the type of the container that holds the repeatable
756+
* annotations; may be {@code null} if the container type should be looked up
757+
* via {@link java.lang.annotation.Repeatable @Repeatable}
750758
* @return the set of all merged repeatable {@code Annotations} found,
751759
* or an empty set if none were found
752760
* @throws IllegalArgumentException if the {@code element} or {@code annotationType}
@@ -775,7 +783,7 @@ private static MergedAnnotations getRepeatableAnnotations(AnnotatedElement eleme
775783

776784
RepeatableContainers repeatableContainers;
777785
if (containerType == null) {
778-
// Invoke RepeatableContainers.of() in order to adhere to the contract of
786+
// Invoke RepeatableContainers.explicitRepeatable() in order to adhere to the contract of
779787
// getMergedRepeatableAnnotations() which states that an IllegalArgumentException
780788
// will be thrown if the container cannot be resolved.
781789
//
@@ -784,11 +792,11 @@ private static MergedAnnotations getRepeatableAnnotations(AnnotatedElement eleme
784792
// annotation types).
785793
//
786794
// See https://github.com/spring-projects/spring-framework/issues/20279
787-
RepeatableContainers.of(annotationType, null);
795+
RepeatableContainers.explicitRepeatable(annotationType, null);
788796
repeatableContainers = RepeatableContainers.standardRepeatables();
789797
}
790798
else {
791-
repeatableContainers = RepeatableContainers.of(annotationType, containerType);
799+
repeatableContainers = RepeatableContainers.explicitRepeatable(annotationType, containerType);
792800
}
793801
return MergedAnnotations.from(element, SearchStrategy.INHERITED_ANNOTATIONS, repeatableContainers);
794802
}
@@ -802,7 +810,7 @@ private static MergedAnnotations findRepeatableAnnotations(AnnotatedElement elem
802810

803811
RepeatableContainers repeatableContainers;
804812
if (containerType == null) {
805-
// Invoke RepeatableContainers.of() in order to adhere to the contract of
813+
// Invoke RepeatableContainers.explicitRepeatable() in order to adhere to the contract of
806814
// findMergedRepeatableAnnotations() which states that an IllegalArgumentException
807815
// will be thrown if the container cannot be resolved.
808816
//
@@ -811,11 +819,11 @@ private static MergedAnnotations findRepeatableAnnotations(AnnotatedElement elem
811819
// annotation types).
812820
//
813821
// See https://github.com/spring-projects/spring-framework/issues/20279
814-
RepeatableContainers.of(annotationType, null);
822+
RepeatableContainers.explicitRepeatable(annotationType, null);
815823
repeatableContainers = RepeatableContainers.standardRepeatables();
816824
}
817825
else {
818-
repeatableContainers = RepeatableContainers.of(annotationType, containerType);
826+
repeatableContainers = RepeatableContainers.explicitRepeatable(annotationType, containerType);
819827
}
820828
return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY, repeatableContainers);
821829
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ public static <A extends Annotation> Set<A> getRepeatableAnnotations(AnnotatedEl
370370
Class<A> annotationType, @Nullable Class<? extends Annotation> containerAnnotationType) {
371371

372372
RepeatableContainers repeatableContainers = (containerAnnotationType != null ?
373-
RepeatableContainers.of(annotationType, containerAnnotationType) :
373+
RepeatableContainers.explicitRepeatable(annotationType, containerAnnotationType) :
374374
RepeatableContainers.standardRepeatables());
375375

376376
return MergedAnnotations.from(annotatedElement, SearchStrategy.SUPERCLASS, repeatableContainers)
@@ -451,7 +451,7 @@ public static <A extends Annotation> Set<A> getDeclaredRepeatableAnnotations(Ann
451451
Class<A> annotationType, @Nullable Class<? extends Annotation> containerAnnotationType) {
452452

453453
RepeatableContainers repeatableContainers = containerAnnotationType != null ?
454-
RepeatableContainers.of(annotationType, containerAnnotationType) :
454+
RepeatableContainers.explicitRepeatable(annotationType, containerAnnotationType) :
455455
RepeatableContainers.standardRepeatables();
456456

457457
return MergedAnnotations.from(annotatedElement, SearchStrategy.DIRECT, repeatableContainers)

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

+126-28
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,38 @@
3030
import org.springframework.util.ObjectUtils;
3131

3232
/**
33-
* Strategy used to determine annotations that act as containers for other
34-
* annotations. The {@link #standardRepeatables()} method provides a default
35-
* strategy that respects Java's {@link Repeatable @Repeatable} support and
36-
* should be suitable for most situations.
33+
* Strategy used to find repeatable annotations within container annotations.
3734
*
38-
* <p>The {@link #of} method can be used to register relationships for
39-
* annotations that do not wish to use {@link Repeatable @Repeatable}.
35+
* <p>{@link #standardRepeatables() RepeatableContainers.standardRepeatables()}
36+
* provides a default strategy that respects Java's {@link Repeatable @Repeatable}
37+
* support and is suitable for most situations.
4038
*
41-
* <p>To completely disable repeatable support use {@link #none()}.
39+
* <p>If you need to register repeatable annotation types that do not make use of
40+
* {@code @Repeatable}, you should typically use {@code standardRepeatables()}
41+
* combined with {@link #plus(Class, Class)}. Note that multiple invocations of
42+
* {@code plus()} can be chained together to register multiple repeatable/container
43+
* type pairs. For example:
44+
*
45+
* <pre class="code">
46+
* RepeatableContainers repeatableContainers =
47+
* RepeatableContainers.standardRepeatables()
48+
* .plus(MyRepeatable1.class, MyContainer1.class)
49+
* .plus(MyRepeatable2.class, MyContainer2.class);</pre>
50+
*
51+
* <p>For special use cases where you are certain that you do not need Java's
52+
* {@code @Repeatable} support, you can use {@link #explicitRepeatable(Class, Class)
53+
* RepeatableContainers.explicitRepeatable()} to create an instance of
54+
* {@code RepeatableContainers} that only supports explicit repeatable/container
55+
* type pairs. As with {@code standardRepeatables()}, {@code plus()} can be used
56+
* to register additional repeatable/container type pairs. For example:
57+
*
58+
* <pre class="code">
59+
* RepeatableContainers repeatableContainers =
60+
* RepeatableContainers.explicitRepeatable(MyRepeatable1.class, MyContainer1.class)
61+
* .plus(MyRepeatable2.class, MyContainer2.class);</pre>
62+
*
63+
* <p>To completely disable repeatable annotation support use
64+
* {@link #none() RepeatableContainers.none()}.
4265
*
4366
* @author Phillip Webb
4467
* @author Sam Brannen
@@ -55,22 +78,46 @@ private RepeatableContainers(@Nullable RepeatableContainers parent) {
5578
this.parent = parent;
5679
}
5780

81+
/**
82+
* Register a pair of repeatable and container annotation types.
83+
* <p>See the {@linkplain RepeatableContainers class-level javadoc} for examples.
84+
* @param repeatable the repeatable annotation type
85+
* @param container the container annotation type
86+
* @return a new {@code RepeatableContainers} instance that is chained to
87+
* the current instance
88+
* @since 7.0
89+
*/
90+
public final RepeatableContainers plus(Class<? extends Annotation> repeatable,
91+
Class<? extends Annotation> container) {
92+
93+
return new ExplicitRepeatableContainer(this, repeatable, container);
94+
}
5895

5996
/**
60-
* Add an additional explicit relationship between a container and
61-
* repeatable annotation.
62-
* <p>WARNING: the arguments supplied to this method are in the reverse order
63-
* of those supplied to {@link #of(Class, Class)}.
97+
* Register a pair of container and repeatable annotation types.
98+
* <p><strong>WARNING</strong>: The arguments supplied to this method are in
99+
* the reverse order of those supplied to {@link #plus(Class, Class)},
100+
* {@link #explicitRepeatable(Class, Class)}, and {@link #of(Class, Class)}.
64101
* @param container the container annotation type
65102
* @param repeatable the repeatable annotation type
66-
* @return a new {@link RepeatableContainers} instance
103+
* @return a new {@code RepeatableContainers} instance that is chained to
104+
* the current instance
105+
* @deprecated as of Spring Framework 7.0, in favor of {@link #plus(Class, Class)}
67106
*/
107+
@Deprecated(since = "7.0")
68108
public RepeatableContainers and(Class<? extends Annotation> container,
69109
Class<? extends Annotation> repeatable) {
70110

71-
return new ExplicitRepeatableContainer(this, repeatable, container);
111+
return plus(repeatable, container);
72112
}
73113

114+
/**
115+
* Find repeated annotations contained in the supplied {@code annotation}.
116+
* @param annotation the candidate container annotation
117+
* @return the repeated annotations found in the supplied container annotation
118+
* (potentially an empty array), or {@code null} if the supplied annotation is
119+
* not a supported container annotation
120+
*/
74121
Annotation @Nullable [] findRepeatedAnnotations(Annotation annotation) {
75122
if (this.parent == null) {
76123
return null;
@@ -98,41 +145,92 @@ public int hashCode() {
98145

99146

100147
/**
101-
* Create a {@link RepeatableContainers} instance that searches using Java's
102-
* {@link Repeatable @Repeatable} annotation.
103-
* @return a {@link RepeatableContainers} instance
148+
* Create a {@link RepeatableContainers} instance that searches for repeated
149+
* annotations according to the semantics of Java's {@link Repeatable @Repeatable}
150+
* annotation.
151+
* <p>See the {@linkplain RepeatableContainers class-level javadoc} for examples.
152+
* @return a {@code RepeatableContainers} instance that supports {@code @Repeatable}
153+
* @see #plus(Class, Class)
104154
*/
105155
public static RepeatableContainers standardRepeatables() {
106156
return StandardRepeatableContainers.INSTANCE;
107157
}
108158

109159
/**
110-
* Create a {@link RepeatableContainers} instance that uses predefined
111-
* repeatable and container types.
112-
* <p>WARNING: the arguments supplied to this method are in the reverse order
113-
* of those supplied to {@link #and(Class, Class)}.
160+
* Create a {@link RepeatableContainers} instance that searches for repeated
161+
* annotations by taking into account the supplied repeatable and container
162+
* annotation types.
163+
* <p><strong>WARNING</strong>: The {@code RepeatableContainers} instance
164+
* returned by this factory method does <strong>not</strong> respect Java's
165+
* {@link Repeatable @Repeatable} support. Use {@link #standardRepeatables()}
166+
* for standard {@code @Repeatable} support, optionally combined with
167+
* {@link #plus(Class, Class)}.
168+
* <p>If the supplied container annotation type is not {@code null}, it must
169+
* declare a {@code value} attribute returning an array of repeatable
170+
* annotations. If the supplied container annotation type is {@code null}, the
171+
* container will be deduced by inspecting the {@code @Repeatable} annotation
172+
* on the {@code repeatable} annotation type.
173+
* <p>See the {@linkplain RepeatableContainers class-level javadoc} for examples.
114174
* @param repeatable the repeatable annotation type
115-
* @param container the container annotation type or {@code null}. If specified,
116-
* this annotation must declare a {@code value} attribute returning an array
117-
* of repeatable annotations. If not specified, the container will be
118-
* deduced by inspecting the {@code @Repeatable} annotation on
119-
* {@code repeatable}.
120-
* @return a {@link RepeatableContainers} instance
175+
* @param container the container annotation type or {@code null}
176+
* @return a {@code RepeatableContainers} instance that does not support
177+
* {@link Repeatable @Repeatable}
121178
* @throws IllegalArgumentException if the supplied container type is
122179
* {@code null} and the annotation type is not a repeatable annotation
123180
* @throws AnnotationConfigurationException if the supplied container type
124181
* is not a properly configured container for a repeatable annotation
182+
* @since 7.0
183+
* @see #standardRepeatables()
184+
* @see #plus(Class, Class)
125185
*/
126-
public static RepeatableContainers of(
186+
public static RepeatableContainers explicitRepeatable(
127187
Class<? extends Annotation> repeatable, @Nullable Class<? extends Annotation> container) {
128188

129189
return new ExplicitRepeatableContainer(null, repeatable, container);
130190
}
131191

192+
/**
193+
* Create a {@link RepeatableContainers} instance that searches for repeated
194+
* annotations by taking into account the supplied repeatable and container
195+
* annotation types.
196+
* <p><strong>WARNING</strong>: The {@code RepeatableContainers} instance
197+
* returned by this factory method does <strong>not</strong> respect Java's
198+
* {@link Repeatable @Repeatable} support. Use {@link #standardRepeatables()}
199+
* for standard {@code @Repeatable} support, optionally combined with
200+
* {@link #plus(Class, Class)}.
201+
* <p><strong>WARNING</strong>: The arguments supplied to this method are in
202+
* the reverse order of those supplied to {@link #and(Class, Class)}.
203+
* <p>If the supplied container annotation type is not {@code null}, it must
204+
* declare a {@code value} attribute returning an array of repeatable
205+
* annotations. If the supplied container annotation type is {@code null}, the
206+
* container will be deduced by inspecting the {@code @Repeatable} annotation
207+
* on the {@code repeatable} annotation type.
208+
* @param repeatable the repeatable annotation type
209+
* @param container the container annotation type or {@code null}
210+
* @return a {@code RepeatableContainers} instance that does not support
211+
* {@link Repeatable @Repeatable}
212+
* @throws IllegalArgumentException if the supplied container type is
213+
* {@code null} and the annotation type is not a repeatable annotation
214+
* @throws AnnotationConfigurationException if the supplied container type
215+
* is not a properly configured container for a repeatable annotation
216+
* @deprecated as of Spring Framework 7.0, in favor of {@link #explicitRepeatable(Class, Class)}
217+
*/
218+
@Deprecated(since = "7.0")
219+
public static RepeatableContainers of(
220+
Class<? extends Annotation> repeatable, @Nullable Class<? extends Annotation> container) {
221+
222+
return explicitRepeatable(repeatable, container);
223+
}
224+
132225
/**
133226
* Create a {@link RepeatableContainers} instance that does not support any
134227
* repeatable annotations.
135-
* @return a {@link RepeatableContainers} instance
228+
* <p>Note, however, that {@link #plus(Class, Class)} may still be invoked on
229+
* the {@code RepeatableContainers} instance returned from this method.
230+
* <p>See the {@linkplain RepeatableContainers class-level javadoc} for examples
231+
* and further details.
232+
* @return a {@code RepeatableContainers} instance that does not support
233+
* repeatable annotations
136234
*/
137235
public static RepeatableContainers none() {
138236
return NoRepeatableContainers.INSTANCE;

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -274,7 +274,7 @@ private <A extends Annotation> Set<A> getAnnotations(Class<? extends Annotation>
274274
private <A extends Annotation> Set<A> getAnnotations(Class<? extends Annotation> container,
275275
Class<A> repeatable, SearchStrategy searchStrategy, AnnotatedElement element, AnnotationFilter annotationFilter) {
276276

277-
RepeatableContainers containers = RepeatableContainers.of(repeatable, container);
277+
RepeatableContainers containers = RepeatableContainers.explicitRepeatable(repeatable, container);
278278
MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy, containers, annotationFilter);
279279
return annotations.stream(repeatable).collect(MergedAnnotationCollectors.toAnnotationSet());
280280
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ void searchFromClassWithCustomAnnotationFilter() {
136136
@Test
137137
void searchFromClassWithCustomRepeatableContainers() {
138138
assertThat(MergedAnnotations.from(HierarchyClass.class).stream(TestConfiguration.class)).isEmpty();
139-
RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, Hierarchy.class);
139+
RepeatableContainers containers = RepeatableContainers.explicitRepeatable(TestConfiguration.class, Hierarchy.class);
140140

141141
MergedAnnotations annotations = MergedAnnotations.search(SearchStrategy.DIRECT)
142142
.withRepeatableContainers(containers)
@@ -1364,7 +1364,7 @@ void streamRepeatableDeclaredOnMethod() throws Exception {
13641364
@SuppressWarnings("deprecation")
13651365
void streamRepeatableDeclaredOnClassWithAttributeAliases() {
13661366
assertThat(MergedAnnotations.from(HierarchyClass.class).stream(TestConfiguration.class)).isEmpty();
1367-
RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, Hierarchy.class);
1367+
RepeatableContainers containers = RepeatableContainers.explicitRepeatable(TestConfiguration.class, Hierarchy.class);
13681368
MergedAnnotations annotations = MergedAnnotations.from(HierarchyClass.class,
13691369
SearchStrategy.DIRECT, containers, AnnotationFilter.NONE);
13701370
assertThat(annotations.stream(TestConfiguration.class)
@@ -1440,7 +1440,7 @@ private void testJavaRepeatables(SearchStrategy searchStrategy, Class<?> element
14401440

14411441
private void testExplicitRepeatables(SearchStrategy searchStrategy, Class<?> element, String[] expected) {
14421442
MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy,
1443-
RepeatableContainers.of(MyRepeatable.class, MyRepeatableContainer.class));
1443+
RepeatableContainers.explicitRepeatable(MyRepeatable.class, MyRepeatableContainer.class));
14441444
Stream<String> values = annotations.stream(MyRepeatable.class)
14451445
.filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
14461446
.map(annotation -> annotation.getString("value"));

0 commit comments

Comments
 (0)