diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java index 35ed4562a6..f48bb77a93 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java @@ -443,13 +443,17 @@ public abstract class AnnotatedElementUtils { * support such a use case, favor {@link #getMergedRepeatableAnnotations(AnnotatedElement, Class)} * over this method or alternatively use the {@link MergedAnnotations} API * directly in conjunction with {@link RepeatableContainers} that are - * {@linkplain RepeatableContainers#and(Class, Class) composed} to support - * multiple repeatable annotation types. + * {@linkplain RepeatableContainers#plus(Class, Class) composed} to support + * multiple repeatable annotation types — for example: + *
+ * RepeatableContainers.standardRepeatables() + * .plus(MyRepeatable1.class, MyContainer1.class) + * .plus(MyRepeatable2.class, MyContainer2.class);* @param element the annotated element (never {@code null}) - * @param annotationType the annotation type to find (never {@code null}) - * @param containerType the type of the container that holds the annotations; - * may be {@code null} if the container type should be looked up via - * {@link java.lang.annotation.Repeatable} + * @param annotationType the repeatable annotation type to find (never {@code null}) + * @param containerType the type of the container that holds the repeatable + * annotations; may be {@code null} if the container type should be looked up + * via {@link java.lang.annotation.Repeatable @Repeatable} * @return the set of all merged repeatable {@code Annotations} found, * or an empty set if none were found * @throws IllegalArgumentException if the {@code element} or {@code annotationType} @@ -740,13 +744,17 @@ public abstract class AnnotatedElementUtils { * support such a use case, favor {@link #findMergedRepeatableAnnotations(AnnotatedElement, Class)} * over this method or alternatively use the {@link MergedAnnotations} API * directly in conjunction with {@link RepeatableContainers} that are - * {@linkplain RepeatableContainers#and(Class, Class) composed} to support - * multiple repeatable annotation types. + * {@linkplain RepeatableContainers#plus(Class, Class) composed} to support + * multiple repeatable annotation types — for example: + *
+ * RepeatableContainers.standardRepeatables() + * .plus(MyRepeatable1.class, MyContainer1.class) + * .plus(MyRepeatable2.class, MyContainer2.class);* @param element the annotated element (never {@code null}) - * @param annotationType the annotation type to find (never {@code null}) - * @param containerType the type of the container that holds the annotations; - * may be {@code null} if the container type should be looked up via - * {@link java.lang.annotation.Repeatable} + * @param annotationType the repeatable annotation type to find (never {@code null}) + * @param containerType the type of the container that holds the repeatable + * annotations; may be {@code null} if the container type should be looked up + * via {@link java.lang.annotation.Repeatable @Repeatable} * @return the set of all merged repeatable {@code Annotations} found, * or an empty set if none were found * @throws IllegalArgumentException if the {@code element} or {@code annotationType} @@ -775,7 +783,7 @@ public abstract class AnnotatedElementUtils { RepeatableContainers repeatableContainers; if (containerType == null) { - // Invoke RepeatableContainers.of() in order to adhere to the contract of + // Invoke RepeatableContainers.explicitRepeatable() in order to adhere to the contract of // getMergedRepeatableAnnotations() which states that an IllegalArgumentException // will be thrown if the container cannot be resolved. // @@ -784,11 +792,11 @@ public abstract class AnnotatedElementUtils { // annotation types). // // See https://github.com/spring-projects/spring-framework/issues/20279 - RepeatableContainers.of(annotationType, null); + RepeatableContainers.explicitRepeatable(annotationType, null); repeatableContainers = RepeatableContainers.standardRepeatables(); } else { - repeatableContainers = RepeatableContainers.of(annotationType, containerType); + repeatableContainers = RepeatableContainers.explicitRepeatable(annotationType, containerType); } return MergedAnnotations.from(element, SearchStrategy.INHERITED_ANNOTATIONS, repeatableContainers); } @@ -802,7 +810,7 @@ public abstract class AnnotatedElementUtils { RepeatableContainers repeatableContainers; if (containerType == null) { - // Invoke RepeatableContainers.of() in order to adhere to the contract of + // Invoke RepeatableContainers.explicitRepeatable() in order to adhere to the contract of // findMergedRepeatableAnnotations() which states that an IllegalArgumentException // will be thrown if the container cannot be resolved. // @@ -811,11 +819,11 @@ public abstract class AnnotatedElementUtils { // annotation types). // // See https://github.com/spring-projects/spring-framework/issues/20279 - RepeatableContainers.of(annotationType, null); + RepeatableContainers.explicitRepeatable(annotationType, null); repeatableContainers = RepeatableContainers.standardRepeatables(); } else { - repeatableContainers = RepeatableContainers.of(annotationType, containerType); + repeatableContainers = RepeatableContainers.explicitRepeatable(annotationType, containerType); } return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY, repeatableContainers); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java index fe46a92076..862cadbf0e 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationUtils.java @@ -370,7 +370,7 @@ public abstract class AnnotationUtils { Class annotationType, @Nullable Class extends Annotation> containerAnnotationType) { RepeatableContainers repeatableContainers = (containerAnnotationType != null ? - RepeatableContainers.of(annotationType, containerAnnotationType) : + RepeatableContainers.explicitRepeatable(annotationType, containerAnnotationType) : RepeatableContainers.standardRepeatables()); return MergedAnnotations.from(annotatedElement, SearchStrategy.SUPERCLASS, repeatableContainers) @@ -451,7 +451,7 @@ public abstract class AnnotationUtils { Class annotationType, @Nullable Class extends Annotation> containerAnnotationType) { RepeatableContainers repeatableContainers = containerAnnotationType != null ? - RepeatableContainers.of(annotationType, containerAnnotationType) : + RepeatableContainers.explicitRepeatable(annotationType, containerAnnotationType) : RepeatableContainers.standardRepeatables(); return MergedAnnotations.from(annotatedElement, SearchStrategy.DIRECT, repeatableContainers) diff --git a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java index ee4cd54d89..82717917b2 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java @@ -30,15 +30,38 @@ import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ObjectUtils; /** - * Strategy used to determine annotations that act as containers for other - * annotations. The {@link #standardRepeatables()} method provides a default - * strategy that respects Java's {@link Repeatable @Repeatable} support and - * should be suitable for most situations. + * Strategy used to find repeatable annotations within container annotations. * - *
The {@link #of} method can be used to register relationships for - * annotations that do not wish to use {@link Repeatable @Repeatable}. + *
{@link #standardRepeatables() RepeatableContainers.standardRepeatables()} + * provides a default strategy that respects Java's {@link Repeatable @Repeatable} + * support and is suitable for most situations. * - *
To completely disable repeatable support use {@link #none()}. + *
If you need to register repeatable annotation types that do not make use of + * {@code @Repeatable}, you should typically use {@code standardRepeatables()} + * combined with {@link #plus(Class, Class)}. Note that multiple invocations of + * {@code plus()} can be chained together to register multiple repeatable/container + * type pairs. For example: + * + *
+ * RepeatableContainers repeatableContainers = + * RepeatableContainers.standardRepeatables() + * .plus(MyRepeatable1.class, MyContainer1.class) + * .plus(MyRepeatable2.class, MyContainer2.class);+ * + *
For special use cases where you are certain that you do not need Java's + * {@code @Repeatable} support, you can use {@link #explicitRepeatable(Class, Class) + * RepeatableContainers.explicitRepeatable()} to create an instance of + * {@code RepeatableContainers} that only supports explicit repeatable/container + * type pairs. As with {@code standardRepeatables()}, {@code plus()} can be used + * to register additional repeatable/container type pairs. For example: + * + *
+ * RepeatableContainers repeatableContainers = + * RepeatableContainers.explicitRepeatable(MyRepeatable1.class, MyContainer1.class) + * .plus(MyRepeatable2.class, MyContainer2.class);+ * + *
To completely disable repeatable annotation support use + * {@link #none() RepeatableContainers.none()}. * * @author Phillip Webb * @author Sam Brannen @@ -55,22 +78,46 @@ public abstract class RepeatableContainers { this.parent = parent; } - /** - * Add an additional explicit relationship between a container and - * repeatable annotation. - *
WARNING: the arguments supplied to this method are in the reverse order - * of those supplied to {@link #of(Class, Class)}. - * @param container the container annotation type + * Register a pair of repeatable and container annotation types. + *
See the {@linkplain RepeatableContainers class-level javadoc} for examples. * @param repeatable the repeatable annotation type - * @return a new {@link RepeatableContainers} instance + * @param container the container annotation type + * @return a new {@code RepeatableContainers} instance that is chained to + * the current instance + * @since 7.0 */ - public RepeatableContainers and(Class extends Annotation> container, - Class extends Annotation> repeatable) { + public final RepeatableContainers plus(Class extends Annotation> repeatable, + Class extends Annotation> container) { return new ExplicitRepeatableContainer(this, repeatable, container); } + /** + * Register a pair of container and repeatable annotation types. + *
WARNING: The arguments supplied to this method are in + * the reverse order of those supplied to {@link #plus(Class, Class)}, + * {@link #explicitRepeatable(Class, Class)}, and {@link #of(Class, Class)}. + * @param container the container annotation type + * @param repeatable the repeatable annotation type + * @return a new {@code RepeatableContainers} instance that is chained to + * the current instance + * @deprecated as of Spring Framework 7.0, in favor of {@link #plus(Class, Class)} + */ + @Deprecated(since = "7.0") + public RepeatableContainers and(Class extends Annotation> container, + Class extends Annotation> repeatable) { + + return plus(repeatable, container); + } + + /** + * Find repeated annotations contained in the supplied {@code annotation}. + * @param annotation the candidate container annotation + * @return the repeated annotations found in the supplied container annotation + * (potentially an empty array), or {@code null} if the supplied annotation is + * not a supported container annotation + */ Annotation @Nullable [] findRepeatedAnnotations(Annotation annotation) { if (this.parent == null) { return null; @@ -98,41 +145,92 @@ public abstract class RepeatableContainers { /** - * Create a {@link RepeatableContainers} instance that searches using Java's - * {@link Repeatable @Repeatable} annotation. - * @return a {@link RepeatableContainers} instance + * Create a {@link RepeatableContainers} instance that searches for repeated + * annotations according to the semantics of Java's {@link Repeatable @Repeatable} + * annotation. + *
See the {@linkplain RepeatableContainers class-level javadoc} for examples. + * @return a {@code RepeatableContainers} instance that supports {@code @Repeatable} + * @see #plus(Class, Class) */ public static RepeatableContainers standardRepeatables() { return StandardRepeatableContainers.INSTANCE; } /** - * Create a {@link RepeatableContainers} instance that uses predefined - * repeatable and container types. - *
WARNING: the arguments supplied to this method are in the reverse order - * of those supplied to {@link #and(Class, Class)}. + * Create a {@link RepeatableContainers} instance that searches for repeated + * annotations by taking into account the supplied repeatable and container + * annotation types. + *
WARNING: The {@code RepeatableContainers} instance + * returned by this factory method does not respect Java's + * {@link Repeatable @Repeatable} support. Use {@link #standardRepeatables()} + * for standard {@code @Repeatable} support, optionally combined with + * {@link #plus(Class, Class)}. + *
If the supplied container annotation type is not {@code null}, it must + * declare a {@code value} attribute returning an array of repeatable + * annotations. If the supplied container annotation type is {@code null}, the + * container will be deduced by inspecting the {@code @Repeatable} annotation + * on the {@code repeatable} annotation type. + *
See the {@linkplain RepeatableContainers class-level javadoc} for examples. * @param repeatable the repeatable annotation type - * @param container the container annotation type or {@code null}. If specified, - * this annotation must declare a {@code value} attribute returning an array - * of repeatable annotations. If not specified, the container will be - * deduced by inspecting the {@code @Repeatable} annotation on - * {@code repeatable}. - * @return a {@link RepeatableContainers} instance + * @param container the container annotation type or {@code null} + * @return a {@code RepeatableContainers} instance that does not support + * {@link Repeatable @Repeatable} * @throws IllegalArgumentException if the supplied container type is * {@code null} and the annotation type is not a repeatable annotation * @throws AnnotationConfigurationException if the supplied container type * is not a properly configured container for a repeatable annotation + * @since 7.0 + * @see #standardRepeatables() + * @see #plus(Class, Class) */ - public static RepeatableContainers of( + public static RepeatableContainers explicitRepeatable( Class extends Annotation> repeatable, @Nullable Class extends Annotation> container) { return new ExplicitRepeatableContainer(null, repeatable, container); } + /** + * Create a {@link RepeatableContainers} instance that searches for repeated + * annotations by taking into account the supplied repeatable and container + * annotation types. + *
WARNING: The {@code RepeatableContainers} instance + * returned by this factory method does not respect Java's + * {@link Repeatable @Repeatable} support. Use {@link #standardRepeatables()} + * for standard {@code @Repeatable} support, optionally combined with + * {@link #plus(Class, Class)}. + *
WARNING: The arguments supplied to this method are in + * the reverse order of those supplied to {@link #and(Class, Class)}. + *
If the supplied container annotation type is not {@code null}, it must + * declare a {@code value} attribute returning an array of repeatable + * annotations. If the supplied container annotation type is {@code null}, the + * container will be deduced by inspecting the {@code @Repeatable} annotation + * on the {@code repeatable} annotation type. + * @param repeatable the repeatable annotation type + * @param container the container annotation type or {@code null} + * @return a {@code RepeatableContainers} instance that does not support + * {@link Repeatable @Repeatable} + * @throws IllegalArgumentException if the supplied container type is + * {@code null} and the annotation type is not a repeatable annotation + * @throws AnnotationConfigurationException if the supplied container type + * is not a properly configured container for a repeatable annotation + * @deprecated as of Spring Framework 7.0, in favor of {@link #explicitRepeatable(Class, Class)} + */ + @Deprecated(since = "7.0") + public static RepeatableContainers of( + Class extends Annotation> repeatable, @Nullable Class extends Annotation> container) { + + return explicitRepeatable(repeatable, container); + } + /** * Create a {@link RepeatableContainers} instance that does not support any * repeatable annotations. - * @return a {@link RepeatableContainers} instance + *
Note, however, that {@link #plus(Class, Class)} may still be invoked on + * the {@code RepeatableContainers} instance returned from this method. + *
See the {@linkplain RepeatableContainers class-level javadoc} for examples
+ * and further details.
+ * @return a {@code RepeatableContainers} instance that does not support
+ * repeatable annotations
*/
public static RepeatableContainers none() {
return NoRepeatableContainers.INSTANCE;
diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java
index 404073b9b4..a734c51a47 100644
--- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java
+++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -274,7 +274,7 @@ class MergedAnnotationsRepeatableAnnotationTests {
private Set getAnnotations(Class extends Annotation> container,
Class repeatable, SearchStrategy searchStrategy, AnnotatedElement element, AnnotationFilter annotationFilter) {
- RepeatableContainers containers = RepeatableContainers.of(repeatable, container);
+ RepeatableContainers containers = RepeatableContainers.explicitRepeatable(repeatable, container);
MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy, containers, annotationFilter);
return annotations.stream(repeatable).collect(MergedAnnotationCollectors.toAnnotationSet());
}
diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java
index 30559d8bdf..65022cb36b 100644
--- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java
+++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java
@@ -136,7 +136,7 @@ class MergedAnnotationsTests {
@Test
void searchFromClassWithCustomRepeatableContainers() {
assertThat(MergedAnnotations.from(HierarchyClass.class).stream(TestConfiguration.class)).isEmpty();
- RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, Hierarchy.class);
+ RepeatableContainers containers = RepeatableContainers.explicitRepeatable(TestConfiguration.class, Hierarchy.class);
MergedAnnotations annotations = MergedAnnotations.search(SearchStrategy.DIRECT)
.withRepeatableContainers(containers)
@@ -1364,7 +1364,7 @@ class MergedAnnotationsTests {
@SuppressWarnings("deprecation")
void streamRepeatableDeclaredOnClassWithAttributeAliases() {
assertThat(MergedAnnotations.from(HierarchyClass.class).stream(TestConfiguration.class)).isEmpty();
- RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, Hierarchy.class);
+ RepeatableContainers containers = RepeatableContainers.explicitRepeatable(TestConfiguration.class, Hierarchy.class);
MergedAnnotations annotations = MergedAnnotations.from(HierarchyClass.class,
SearchStrategy.DIRECT, containers, AnnotationFilter.NONE);
assertThat(annotations.stream(TestConfiguration.class)
@@ -1440,7 +1440,7 @@ class MergedAnnotationsTests {
private void testExplicitRepeatables(SearchStrategy searchStrategy, Class> element, String[] expected) {
MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy,
- RepeatableContainers.of(MyRepeatable.class, MyRepeatableContainer.class));
+ RepeatableContainers.explicitRepeatable(MyRepeatable.class, MyRepeatableContainer.class));
Stream