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
This commit is contained in:
Sam Brannen 2022-03-22 19:42:08 +01:00
parent 565ffd129a
commit c23edf7da6
2 changed files with 196 additions and 5 deletions

View File

@ -91,8 +91,14 @@ import org.springframework.util.Assert;
* *
* <p>Different {@linkplain SearchStrategy search strategies} can be used to locate * <p>Different {@linkplain SearchStrategy search strategies} can be used to locate
* related source elements that contain the annotations to be aggregated. For * related source elements that contain the annotations to be aggregated. For
* example, {@link SearchStrategy#TYPE_HIERARCHY} will search both superclasses and * example, the following code uses {@link SearchStrategy#TYPE_HIERARCHY} to
* implemented interfaces. * search for annotations on {@code MyClass} as well as in superclasses and implemented
* interfaces.
*
* <pre class="code">
* MergedAnnotations mergedAnnotations =
* MergedAnnotations.search(TYPE_HIERARCHY).from(MyClass.class);
* </pre>
* *
* <p>From a {@code MergedAnnotations} instance you can either * <p>From a {@code MergedAnnotations} instance you can either
* {@linkplain #get(String) get} a single annotation, or {@linkplain #stream() * {@linkplain #get(String) get} a single annotation, or {@linkplain #stream()
@ -295,6 +301,7 @@ public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>
* @param element the source element * @param element the source element
* @return a {@code MergedAnnotations} instance containing the element's * @return a {@code MergedAnnotations} instance containing the element's
* annotations * annotations
* @see #search(SearchStrategy)
*/ */
static MergedAnnotations from(AnnotatedElement element) { static MergedAnnotations from(AnnotatedElement element) {
return from(element, SearchStrategy.DIRECT); return from(element, SearchStrategy.DIRECT);
@ -308,6 +315,7 @@ public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>
* @param searchStrategy the search strategy to use * @param searchStrategy the search strategy to use
* @return a {@code MergedAnnotations} instance containing the merged * @return a {@code MergedAnnotations} instance containing the merged
* element annotations * element annotations
* @see #search(SearchStrategy)
*/ */
static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy) { static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy) {
return from(element, searchStrategy, RepeatableContainers.standardRepeatables()); return from(element, searchStrategy, RepeatableContainers.standardRepeatables());
@ -323,6 +331,7 @@ public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>
* the element annotations or the meta-annotations * the element annotations or the meta-annotations
* @return a {@code MergedAnnotations} instance containing the merged * @return a {@code MergedAnnotations} instance containing the merged
* element annotations * element annotations
* @see #search(SearchStrategy)
*/ */
static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy, static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy,
RepeatableContainers repeatableContainers) { RepeatableContainers repeatableContainers) {
@ -342,10 +351,13 @@ public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>
* annotations considered * annotations considered
* @return a {@code MergedAnnotations} instance containing the merged * @return a {@code MergedAnnotations} instance containing the merged
* annotations for the supplied element * annotations for the supplied element
* @see #search(SearchStrategy)
*/ */
static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy, static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy,
RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) { RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) {
Assert.notNull(element, "AnnotatedElement must not be null");
Assert.notNull(searchStrategy, "SearchStrategy must not be null");
Assert.notNull(repeatableContainers, "RepeatableContainers must not be null"); Assert.notNull(repeatableContainers, "RepeatableContainers must not be null");
Assert.notNull(annotationFilter, "AnnotationFilter must not be null"); Assert.notNull(annotationFilter, "AnnotationFilter must not be null");
return TypeMappedAnnotations.from(element, searchStrategy, repeatableContainers, annotationFilter); return TypeMappedAnnotations.from(element, searchStrategy, repeatableContainers, annotationFilter);
@ -432,11 +444,127 @@ public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>
return MergedAnnotationsCollection.of(annotations); return MergedAnnotationsCollection.of(annotations);
} }
/**
* Find merged annotations using the supplied {@link SearchStrategy} and a
* fluent API for configuring and performing the search.
* <p>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 * Fluent API for configuring the search algorithm used in the
* {@link MergedAnnotations#from(AnnotatedElement, SearchStrategy)} and * {@link MergedAnnotations} model and performing a search.
* variants of that method. *
* <ul>
* <li>Configuration starts with an invocation of
* {@link MergedAnnotations#search(SearchStrategy)}, specifying which
* {@link SearchStrategy} to use.</li>
* <li>Optional configuration can be provided via one of the {@code with*()}
* methods.</li>
* <li>The actual search is performed by invoking {@link #from(AnnotatedElement)}
* with the source element from which the search should begin.</li>
* </ul>
*
* <p>For example, the following performs a search on {@code MyClass} within
* the entire type hierarchy of that class while ignoring repeatable annotations.
*
* <pre class="code">
* MergedAnnotations mergedAnnotations =
* MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
* .withRepeatableContainers(RepeatableContainers.none())
* .from(MyClass.class);
* </pre>
*
* <p>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.
*
* <pre class="code">
* 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
* </pre>
*
* @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.
* <p>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.
* <p>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.
* *
* <p>Each strategy creates a different set of aggregates that will be * <p>Each strategy creates a different set of aggregates that will be
* combined to create the final {@link MergedAnnotations}. * combined to create the final {@link MergedAnnotations}.

View File

@ -36,10 +36,12 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.MergedAnnotation.Adapt; 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.MergedAnnotations.SearchStrategy;
import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass; import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass;
import org.springframework.core.testfixture.stereotype.Component; import org.springframework.core.testfixture.stereotype.Component;
@ -73,6 +75,67 @@ import static org.assertj.core.api.Assertions.entry;
*/ */
class MergedAnnotationsTests { 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<Class<?>> classes = MergedAnnotations.search(SearchStrategy.DIRECT)
.from(TransactionalComponent.class)
.stream()
.map(MergedAnnotation::getType);
assertThat(classes).containsExactly(Transactional.class, Component.class, Indexed.class);
}
@Test
void searchOnClassWithCustomAnnotationFilter() {
Stream<Class<?>> classes = MergedAnnotations.search(SearchStrategy.DIRECT)
.withAnnotationFilter(annotationName -> annotationName.endsWith("Indexed"))
.from(TransactionalComponent.class)
.stream()
.map(MergedAnnotation::getType);
assertThat(classes).containsExactly(Transactional.class, Component.class);
}
@Test
void searchOnClassWithCustomRepeatableContainers() {
assertThat(MergedAnnotations.from(HierarchyClass.class).stream(TestConfiguration.class)).isEmpty();
RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, Hierarchy.class);
MergedAnnotations annotations = MergedAnnotations.search(SearchStrategy.DIRECT)
.withRepeatableContainers(containers)
.from(HierarchyClass.class);
assertThat(annotations.stream(TestConfiguration.class).map(annotation -> annotation.getString("location")))
.containsExactly("A", "B");
assertThat(annotations.stream(TestConfiguration.class).map(annotation -> annotation.getString("value")))
.containsExactly("A", "B");
}
}
@Test @Test
void fromPreconditions() { void fromPreconditions() {
SearchStrategy strategy = SearchStrategy.DIRECT; SearchStrategy strategy = SearchStrategy.DIRECT;