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

@@ -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<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
void fromPreconditions() {
SearchStrategy strategy = SearchStrategy.DIRECT;