Skip to content

Commit c23edf7

Browse files
committed
Introduce fluent API for searches in MergedAnnotations
Prior to this commit, searching for merged annotations on an AnnotatedElement in the MergedAnnotations model was only supported via various overloaded from(...) factory methods. In addition, it was not possible to provide a custom AnnotationFilter without providing an instance of RepeatableContainers. This commit introduces a fluent API for searches in MergedAnnotations to address these issues and improve the programming model for users of MergedAnnotations. To begin a search, invoke MergedAnnotations.search(SearchStrategy) with the desired search strategy. Optional configuration can then be provided via one of the with(...) methods. To perform a search, invoke from(AnnotatedElement), supplying the element from which to begin the search -- for example, a Class or a Method. For example, the following performs a search on MyClass within the entire type hierarchy of that class while ignoring repeatable annotations. MergedAnnotations mergedAnnotations = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY) .withRepeatableContainers(RepeatableContainers.none()) .from(MyClass.class); To reuse search configuration to perform the same type of search on multiple elements, you can save the Search instance as demonstrated in the following example. 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 In addition, this fluent search API paves the way for introducing support for a predicate that controls the search on enclosing classes (gh-28207) and subsequently for completely removing the TYPE_HIERARCHY_AND_ENCLOSING_CLASSES search strategy (gh-28080). Closes gh-28208
1 parent 565ffd1 commit c23edf7

File tree

2 files changed

+196
-5
lines changed

2 files changed

+196
-5
lines changed

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

+133-5
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,14 @@
9191
*
9292
* <p>Different {@linkplain SearchStrategy search strategies} can be used to locate
9393
* related source elements that contain the annotations to be aggregated. For
94-
* example, {@link SearchStrategy#TYPE_HIERARCHY} will search both superclasses and
95-
* implemented interfaces.
94+
* example, the following code uses {@link SearchStrategy#TYPE_HIERARCHY} to
95+
* search for annotations on {@code MyClass} as well as in superclasses and implemented
96+
* interfaces.
97+
*
98+
* <pre class="code">
99+
* MergedAnnotations mergedAnnotations =
100+
* MergedAnnotations.search(TYPE_HIERARCHY).from(MyClass.class);
101+
* </pre>
96102
*
97103
* <p>From a {@code MergedAnnotations} instance you can either
98104
* {@linkplain #get(String) get} a single annotation, or {@linkplain #stream()
@@ -295,6 +301,7 @@ <A extends Annotation> MergedAnnotation<A> get(String annotationType,
295301
* @param element the source element
296302
* @return a {@code MergedAnnotations} instance containing the element's
297303
* annotations
304+
* @see #search(SearchStrategy)
298305
*/
299306
static MergedAnnotations from(AnnotatedElement element) {
300307
return from(element, SearchStrategy.DIRECT);
@@ -308,6 +315,7 @@ static MergedAnnotations from(AnnotatedElement element) {
308315
* @param searchStrategy the search strategy to use
309316
* @return a {@code MergedAnnotations} instance containing the merged
310317
* element annotations
318+
* @see #search(SearchStrategy)
311319
*/
312320
static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy) {
313321
return from(element, searchStrategy, RepeatableContainers.standardRepeatables());
@@ -323,6 +331,7 @@ static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStr
323331
* the element annotations or the meta-annotations
324332
* @return a {@code MergedAnnotations} instance containing the merged
325333
* element annotations
334+
* @see #search(SearchStrategy)
326335
*/
327336
static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy,
328337
RepeatableContainers repeatableContainers) {
@@ -342,10 +351,13 @@ static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStr
342351
* annotations considered
343352
* @return a {@code MergedAnnotations} instance containing the merged
344353
* annotations for the supplied element
354+
* @see #search(SearchStrategy)
345355
*/
346356
static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy,
347357
RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) {
348358

359+
Assert.notNull(element, "AnnotatedElement must not be null");
360+
Assert.notNull(searchStrategy, "SearchStrategy must not be null");
349361
Assert.notNull(repeatableContainers, "RepeatableContainers must not be null");
350362
Assert.notNull(annotationFilter, "AnnotationFilter must not be null");
351363
return TypeMappedAnnotations.from(element, searchStrategy, repeatableContainers, annotationFilter);
@@ -432,11 +444,127 @@ static MergedAnnotations of(Collection<MergedAnnotation<?>> annotations) {
432444
return MergedAnnotationsCollection.of(annotations);
433445
}
434446

447+
/**
448+
* Find merged annotations using the supplied {@link SearchStrategy} and a
449+
* fluent API for configuring and performing the search.
450+
* <p>See {@link Search} for details.
451+
* @param searchStrategy the search strategy to use
452+
* @return a {@code Search} instance to perform the search
453+
* @since 6.0
454+
*/
455+
static Search search(SearchStrategy searchStrategy) {
456+
Assert.notNull(searchStrategy, "SearchStrategy must not be null");
457+
return new Search(searchStrategy);
458+
}
459+
460+
461+
/**
462+
* Fluent API for configuring the search algorithm used in the
463+
* {@link MergedAnnotations} model and performing a search.
464+
*
465+
* <ul>
466+
* <li>Configuration starts with an invocation of
467+
* {@link MergedAnnotations#search(SearchStrategy)}, specifying which
468+
* {@link SearchStrategy} to use.</li>
469+
* <li>Optional configuration can be provided via one of the {@code with*()}
470+
* methods.</li>
471+
* <li>The actual search is performed by invoking {@link #from(AnnotatedElement)}
472+
* with the source element from which the search should begin.</li>
473+
* </ul>
474+
*
475+
* <p>For example, the following performs a search on {@code MyClass} within
476+
* the entire type hierarchy of that class while ignoring repeatable annotations.
477+
*
478+
* <pre class="code">
479+
* MergedAnnotations mergedAnnotations =
480+
* MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
481+
* .withRepeatableContainers(RepeatableContainers.none())
482+
* .from(MyClass.class);
483+
* </pre>
484+
*
485+
* <p>If you wish to reuse search configuration to perform the same type of search
486+
* on multiple elements, you can save the {@code Search} instance as demonstrated
487+
* in the following example.
488+
*
489+
* <pre class="code">
490+
* Search search = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
491+
* .withRepeatableContainers(RepeatableContainers.none());
492+
*
493+
* MergedAnnotations mergedAnnotations = search.from(MyClass.class);
494+
* // do something with the MergedAnnotations for MyClass
495+
* mergedAnnotations = search.from(AnotherClass.class);
496+
* // do something with the MergedAnnotations for AnotherClass
497+
* </pre>
498+
*
499+
* @since 6.0
500+
*/
501+
static final class Search {
502+
503+
private final SearchStrategy searchStrategy;
504+
505+
private RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables();
506+
507+
private AnnotationFilter annotationFilter = AnnotationFilter.PLAIN;
508+
509+
510+
private Search(SearchStrategy searchStrategy) {
511+
this.searchStrategy = searchStrategy;
512+
}
513+
514+
515+
/**
516+
* Configure the {@link RepeatableContainers} to use.
517+
* <p>Defaults to {@link RepeatableContainers#standardRepeatables()}.
518+
* @param repeatableContainers the repeatable containers that may be used
519+
* by annotations or meta-annotations
520+
* @return this {@code Search} instance for chained method invocations
521+
* @see #withAnnotationFilter(AnnotationFilter)
522+
* @see #from(AnnotatedElement)
523+
*/
524+
public Search withRepeatableContainers(RepeatableContainers repeatableContainers) {
525+
Assert.notNull(repeatableContainers, "RepeatableContainers must not be null");
526+
this.repeatableContainers = repeatableContainers;
527+
return this;
528+
}
529+
530+
/**
531+
* Configure the {@link AnnotationFilter} to use.
532+
* <p>Defaults to {@link AnnotationFilter#PLAIN}.
533+
* @param annotationFilter an annotation filter used to restrict the
534+
* annotations considered
535+
* @return this {@code Search} instance for chained method invocations
536+
* @see #withRepeatableContainers(RepeatableContainers)
537+
* @see #from(AnnotatedElement)
538+
*/
539+
public Search withAnnotationFilter(AnnotationFilter annotationFilter) {
540+
Assert.notNull(annotationFilter, "AnnotationFilter must not be null");
541+
this.annotationFilter = annotationFilter;
542+
return this;
543+
}
544+
545+
/**
546+
* Perform a search for merged annotations beginning with the supplied
547+
* {@link AnnotatedElement} (such as a {@link Class} or {@link Method}),
548+
* using the configuration in this {@code Search} instance.
549+
* @param element the source element
550+
* @return a new {@link MergedAnnotations} instance containing all
551+
* annotations and meta-annotations from the specified element and,
552+
* depending on the {@link SearchStrategy}, related inherited elements
553+
* @see #withRepeatableContainers(RepeatableContainers)
554+
* @see #withAnnotationFilter(AnnotationFilter)
555+
* @see MergedAnnotations#from(AnnotatedElement, SearchStrategy, RepeatableContainers, AnnotationFilter)
556+
*/
557+
public MergedAnnotations from(AnnotatedElement element) {
558+
return MergedAnnotations.from(element, this.searchStrategy, this.repeatableContainers,
559+
this.annotationFilter);
560+
}
561+
562+
}
435563

436564
/**
437-
* Search strategies supported by
438-
* {@link MergedAnnotations#from(AnnotatedElement, SearchStrategy)} and
439-
* variants of that method.
565+
* Search strategies supported by {@link MergedAnnotations#search(SearchStrategy)}
566+
* as well as {@link MergedAnnotations#from(AnnotatedElement, SearchStrategy)}
567+
* and variants of that method.
440568
*
441569
* <p>Each strategy creates a different set of aggregates that will be
442570
* combined to create the final {@link MergedAnnotations}.

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

+63
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@
3636
import java.util.stream.Stream;
3737

3838
import jakarta.annotation.Resource;
39+
import org.junit.jupiter.api.Nested;
3940
import org.junit.jupiter.api.Test;
4041

4142
import org.springframework.core.Ordered;
4243
import org.springframework.core.annotation.MergedAnnotation.Adapt;
44+
import org.springframework.core.annotation.MergedAnnotations.Search;
4345
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
4446
import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass;
4547
import org.springframework.core.testfixture.stereotype.Component;
@@ -73,6 +75,67 @@
7375
*/
7476
class MergedAnnotationsTests {
7577

78+
/**
79+
* Subset (and duplication) of other tests in {@link MergedAnnotationsTests}
80+
* that verify behavior of the fluent {@link Search} API.
81+
* @since 6.0
82+
*/
83+
@Nested
84+
class FluentSearchApiTests {
85+
86+
@Test
87+
void preconditions() {
88+
assertThatIllegalArgumentException()
89+
.isThrownBy(() -> MergedAnnotations.search(null))
90+
.withMessage("SearchStrategy must not be null");
91+
92+
Search search = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY);
93+
assertThatIllegalArgumentException()
94+
.isThrownBy(() -> search.withAnnotationFilter(null))
95+
.withMessage("AnnotationFilter must not be null");
96+
assertThatIllegalArgumentException()
97+
.isThrownBy(() -> search.withRepeatableContainers(null))
98+
.withMessage("RepeatableContainers must not be null");
99+
assertThatIllegalArgumentException()
100+
.isThrownBy(() -> search.from(null))
101+
.withMessage("AnnotatedElement must not be null");
102+
}
103+
104+
@Test
105+
void searchOnClassWithDefaultAnnotationFilterAndRepeatableContainers() {
106+
Stream<Class<?>> classes = MergedAnnotations.search(SearchStrategy.DIRECT)
107+
.from(TransactionalComponent.class)
108+
.stream()
109+
.map(MergedAnnotation::getType);
110+
assertThat(classes).containsExactly(Transactional.class, Component.class, Indexed.class);
111+
}
112+
113+
@Test
114+
void searchOnClassWithCustomAnnotationFilter() {
115+
Stream<Class<?>> classes = MergedAnnotations.search(SearchStrategy.DIRECT)
116+
.withAnnotationFilter(annotationName -> annotationName.endsWith("Indexed"))
117+
.from(TransactionalComponent.class)
118+
.stream()
119+
.map(MergedAnnotation::getType);
120+
assertThat(classes).containsExactly(Transactional.class, Component.class);
121+
}
122+
123+
@Test
124+
void searchOnClassWithCustomRepeatableContainers() {
125+
assertThat(MergedAnnotations.from(HierarchyClass.class).stream(TestConfiguration.class)).isEmpty();
126+
RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, Hierarchy.class);
127+
128+
MergedAnnotations annotations = MergedAnnotations.search(SearchStrategy.DIRECT)
129+
.withRepeatableContainers(containers)
130+
.from(HierarchyClass.class);
131+
assertThat(annotations.stream(TestConfiguration.class).map(annotation -> annotation.getString("location")))
132+
.containsExactly("A", "B");
133+
assertThat(annotations.stream(TestConfiguration.class).map(annotation -> annotation.getString("value")))
134+
.containsExactly("A", "B");
135+
}
136+
137+
}
138+
76139
@Test
77140
void fromPreconditions() {
78141
SearchStrategy strategy = SearchStrategy.DIRECT;

0 commit comments

Comments
 (0)