diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java index 9800505d77..3614514820 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java @@ -91,8 +91,14 @@ import org.springframework.util.Assert; * *
Different {@linkplain SearchStrategy search strategies} can be used to locate * related source elements that contain the annotations to be aggregated. For - * example, {@link SearchStrategy#TYPE_HIERARCHY} will search both superclasses and - * implemented interfaces. + * example, the following code uses {@link SearchStrategy#TYPE_HIERARCHY} to + * search for annotations on {@code MyClass} as well as in superclasses and implemented + * interfaces. + * + *
+ * MergedAnnotations mergedAnnotations = + * MergedAnnotations.search(TYPE_HIERARCHY).from(MyClass.class); + ** *
From a {@code MergedAnnotations} instance you can either
* {@linkplain #get(String) get} a single annotation, or {@linkplain #stream()
@@ -295,6 +301,7 @@ public interface MergedAnnotations extends Iterable See {@link Search} for details.
+ * @param searchStrategy the search strategy to use
+ * @return a {@code Search} instance to perform the search
+ * @since 6.0
+ */
+ static Search search(SearchStrategy searchStrategy) {
+ Assert.notNull(searchStrategy, "SearchStrategy must not be null");
+ return new Search(searchStrategy);
+ }
+
/**
- * Search strategies supported by
- * {@link MergedAnnotations#from(AnnotatedElement, SearchStrategy)} and
- * variants of that method.
+ * Fluent API for configuring the search algorithm used in the
+ * {@link MergedAnnotations} model and performing a search.
+ *
+ * For example, the following performs a search on {@code MyClass} within
+ * the entire type hierarchy of that class while ignoring repeatable annotations.
+ *
+ * If you wish to reuse search configuration to perform the same type of search
+ * on multiple elements, you can save the {@code Search} instance as demonstrated
+ * in the following example.
+ *
+ * Defaults to {@link RepeatableContainers#standardRepeatables()}.
+ * @param repeatableContainers the repeatable containers that may be used
+ * by annotations or meta-annotations
+ * @return this {@code Search} instance for chained method invocations
+ * @see #withAnnotationFilter(AnnotationFilter)
+ * @see #from(AnnotatedElement)
+ */
+ public Search withRepeatableContainers(RepeatableContainers repeatableContainers) {
+ Assert.notNull(repeatableContainers, "RepeatableContainers must not be null");
+ this.repeatableContainers = repeatableContainers;
+ return this;
+ }
+
+ /**
+ * Configure the {@link AnnotationFilter} to use.
+ * Defaults to {@link AnnotationFilter#PLAIN}.
+ * @param annotationFilter an annotation filter used to restrict the
+ * annotations considered
+ * @return this {@code Search} instance for chained method invocations
+ * @see #withRepeatableContainers(RepeatableContainers)
+ * @see #from(AnnotatedElement)
+ */
+ public Search withAnnotationFilter(AnnotationFilter annotationFilter) {
+ Assert.notNull(annotationFilter, "AnnotationFilter must not be null");
+ this.annotationFilter = annotationFilter;
+ return this;
+ }
+
+ /**
+ * Perform a search for merged annotations beginning with the supplied
+ * {@link AnnotatedElement} (such as a {@link Class} or {@link Method}),
+ * using the configuration in this {@code Search} instance.
+ * @param element the source element
+ * @return a new {@link MergedAnnotations} instance containing all
+ * annotations and meta-annotations from the specified element and,
+ * depending on the {@link SearchStrategy}, related inherited elements
+ * @see #withRepeatableContainers(RepeatableContainers)
+ * @see #withAnnotationFilter(AnnotationFilter)
+ * @see MergedAnnotations#from(AnnotatedElement, SearchStrategy, RepeatableContainers, AnnotationFilter)
+ */
+ public MergedAnnotations from(AnnotatedElement element) {
+ return MergedAnnotations.from(element, this.searchStrategy, this.repeatableContainers,
+ this.annotationFilter);
+ }
+
+ }
+
+ /**
+ * Search strategies supported by {@link MergedAnnotations#search(SearchStrategy)}
+ * as well as {@link MergedAnnotations#from(AnnotatedElement, SearchStrategy)}
+ * and variants of that method.
*
* Each strategy creates a different set of aggregates that will be
* combined to create the final {@link MergedAnnotations}.
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 03666ff884..2daaf981bf 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
@@ -36,10 +36,12 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.annotation.Resource;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.MergedAnnotation.Adapt;
+import org.springframework.core.annotation.MergedAnnotations.Search;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass;
import org.springframework.core.testfixture.stereotype.Component;
@@ -73,6 +75,67 @@ import static org.assertj.core.api.Assertions.entry;
*/
class MergedAnnotationsTests {
+ /**
+ * Subset (and duplication) of other tests in {@link MergedAnnotationsTests}
+ * that verify behavior of the fluent {@link Search} API.
+ * @since 6.0
+ */
+ @Nested
+ class FluentSearchApiTests {
+
+ @Test
+ void preconditions() {
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> MergedAnnotations.search(null))
+ .withMessage("SearchStrategy must not be null");
+
+ Search search = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY);
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> search.withAnnotationFilter(null))
+ .withMessage("AnnotationFilter must not be null");
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> search.withRepeatableContainers(null))
+ .withMessage("RepeatableContainers must not be null");
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> search.from(null))
+ .withMessage("AnnotatedElement must not be null");
+ }
+
+ @Test
+ void searchOnClassWithDefaultAnnotationFilterAndRepeatableContainers() {
+ Stream
+ *
+ *
+ *
+ * MergedAnnotations mergedAnnotations =
+ * MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
+ * .withRepeatableContainers(RepeatableContainers.none())
+ * .from(MyClass.class);
+ *
+ *
+ *
+ * Search search = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
+ * .withRepeatableContainers(RepeatableContainers.none());
+ *
+ * MergedAnnotations mergedAnnotations = search.from(MyClass.class);
+ * // do something with the MergedAnnotations for MyClass
+ * mergedAnnotations = search.from(AnotherClass.class);
+ * // do something with the MergedAnnotations for AnotherClass
+ *
+ *
+ * @since 6.0
+ */
+ static final class Search {
+
+ private final SearchStrategy searchStrategy;
+
+ private RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables();
+
+ private AnnotationFilter annotationFilter = AnnotationFilter.PLAIN;
+
+
+ private Search(SearchStrategy searchStrategy) {
+ this.searchStrategy = searchStrategy;
+ }
+
+
+ /**
+ * Configure the {@link RepeatableContainers} to use.
+ *