Introduce predicate for searching enclosing classes in MergedAnnotations

Due to the deprecation of the TYPE_HIERARCHY_AND_ENCLOSING_CLASSES
search strategy (see gh-28079), this commit introduces a way for users
to provide a Predicate<Class<?>> that is used to decide when the
enclosing class for the class supplied to the predicate should be
searched.

This gives the user complete control over the "enclosing classes"
aspect of the search algorithm in MergedAnnotations.

- To achieve the same behavior as TYPE_HIERARCHY_AND_ENCLOSING_CLASSES,
  a user can provide `clazz -> true` as the predicate.

- To limit the enclosing class search to inner classes, a user can
  provide `ClassUtils::isInnerClass` as the predicate.

- To limit the enclosing class search to static nested classes, a user
  can provide `ClassUtils::isStaticClass` as the predicate.

- For more advanced use cases, the user can provide a custom predicate.

For example, the following performs a search on MyInnerClass within the
entire type hierarchy and enclosing class hierarchy of that class.

MergedAnnotations mergedAnnotations =
   MergedAnnotations.search(TYPE_HIERARCHY)
      .withEnclosingClasses(ClassUtils::isInnerClass)
      .from(MyInnerClass.class);

In addition, TestContextAnnotationUtils in spring-test has been
revised to use this new feature where feasible.

Closes gh-28207
This commit is contained in:
Sam Brannen
2022-03-24 15:22:21 +01:00
parent 7161940b53
commit 1fe394f11d
6 changed files with 217 additions and 51 deletions

View File

@@ -30,6 +30,7 @@ import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.core.annotation.MergedAnnotations.Search;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
@@ -448,8 +449,8 @@ class AnnotationsScannerTests {
@Test
void scanWhenProcessorReturnsFromDoWithAggregateExitsEarly() {
String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class,
SearchStrategy.TYPE_HIERARCHY, new AnnotationsProcessor<Object, String>() {
String result = scan(this, WithSingleSuperclass.class, SearchStrategy.TYPE_HIERARCHY,
new AnnotationsProcessor<Object, String>() {
@Override
@Nullable
@@ -471,8 +472,7 @@ class AnnotationsScannerTests {
@Test
void scanWhenProcessorReturnsFromDoWithAnnotationsExitsEarly() {
List<Integer> indexes = new ArrayList<>();
String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class,
SearchStrategy.TYPE_HIERARCHY,
String result = scan(this, WithSingleSuperclass.class, SearchStrategy.TYPE_HIERARCHY,
(context, aggregateIndex, source, annotations) -> {
indexes.add(aggregateIndex);
return "";
@@ -483,8 +483,8 @@ class AnnotationsScannerTests {
@Test
void scanWhenProcessorHasFinishMethodUsesFinishResult() {
String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class,
SearchStrategy.TYPE_HIERARCHY, new AnnotationsProcessor<Object, String>() {
String result = scan(this, WithSingleSuperclass.class, SearchStrategy.TYPE_HIERARCHY,
new AnnotationsProcessor<Object, String>() {
@Override
@Nullable
@@ -510,7 +510,7 @@ class AnnotationsScannerTests {
private Stream<String> scan(AnnotatedElement element, SearchStrategy searchStrategy) {
List<String> results = new ArrayList<>();
AnnotationsScanner.scan(this, element, searchStrategy,
scan(this, element, searchStrategy,
(criteria, aggregateIndex, source, annotations) -> {
trackIndexedAnnotations(aggregateIndex, annotations, results);
return null; // continue searching
@@ -518,6 +518,12 @@ class AnnotationsScannerTests {
return results.stream();
}
private static <C, R> R scan(C context, AnnotatedElement source, SearchStrategy searchStrategy,
AnnotationsProcessor<C, R> processor) {
return AnnotationsScanner.scan(context, source, searchStrategy, Search.never, processor);
}
private void trackIndexedAnnotations(int aggregateIndex, Annotation[] annotations, List<String> results) {
Arrays.stream(annotations)
.filter(Objects::nonNull)

View File

@@ -89,20 +89,30 @@ class MergedAnnotationsTests {
.isThrownBy(() -> MergedAnnotations.search(null))
.withMessage("SearchStrategy must not be null");
Search search = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY);
Search search = MergedAnnotations.search(SearchStrategy.SUPERCLASS);
assertThatIllegalArgumentException()
.isThrownBy(() -> search.withEnclosingClasses(null))
.withMessage("Predicate must not be null");
assertThatIllegalStateException()
.isThrownBy(() -> search.withEnclosingClasses(Search.always))
.withMessage("A custom 'searchEnclosingClass' predicate can only be combined with 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() {
void searchFromClassWithDefaultAnnotationFilterAndDefaultRepeatableContainers() {
Stream<Class<?>> classes = MergedAnnotations.search(SearchStrategy.DIRECT)
.from(TransactionalComponent.class)
.stream()
@@ -111,7 +121,7 @@ class MergedAnnotationsTests {
}
@Test
void searchOnClassWithCustomAnnotationFilter() {
void searchFromClassWithCustomAnnotationFilter() {
Stream<Class<?>> classes = MergedAnnotations.search(SearchStrategy.DIRECT)
.withAnnotationFilter(annotationName -> annotationName.endsWith("Indexed"))
.from(TransactionalComponent.class)
@@ -121,19 +131,79 @@ class MergedAnnotationsTests {
}
@Test
void searchOnClassWithCustomRepeatableContainers() {
void searchFromClassWithCustomRepeatableContainers() {
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")))
assertThat(annotations.stream(TestConfiguration.class))
.map(annotation -> annotation.getString("location"))
.containsExactly("A", "B");
assertThat(annotations.stream(TestConfiguration.class).map(annotation -> annotation.getString("value")))
assertThat(annotations.stream(TestConfiguration.class))
.map(annotation -> annotation.getString("value"))
.containsExactly("A", "B");
}
/**
* @since 6.0
*/
@Test
void searchFromNonAnnotatedInnerClassWithAnnotatedEnclosingClassWithEnclosingClassPredicates() {
Class<?> testCase = AnnotatedClass.NonAnnotatedInnerClass.class;
Search search = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY);
assertThat(search.from(testCase).stream()).isEmpty();
assertThat(search.withEnclosingClasses(Search.never).from(testCase).stream()).isEmpty();
assertThat(search.withEnclosingClasses(ClassUtils::isStaticClass).from(testCase).stream()).isEmpty();
Stream<Class<?>> classes = search.withEnclosingClasses(ClassUtils::isInnerClass)
.from(testCase)
.stream()
.map(MergedAnnotation::getType);
assertThat(classes).containsExactly(Component.class, Indexed.class);
classes = search.withEnclosingClasses(Search.always)
.from(testCase)
.stream()
.map(MergedAnnotation::getType);
assertThat(classes).containsExactly(Component.class, Indexed.class);
classes = search.withEnclosingClasses(ClassUtils::isInnerClass)
.withRepeatableContainers(RepeatableContainers.none())
.withAnnotationFilter(annotationName -> annotationName.endsWith("Indexed"))
.from(testCase)
.stream()
.map(MergedAnnotation::getType);
assertThat(classes).containsExactly(Component.class);
}
/**
* @since 6.0
*/
@Test
void searchFromNonAnnotatedStaticNestedClassWithAnnotatedEnclosingClassWithEnclosingClassPredicates() {
Class<?> testCase = AnnotatedClass.NonAnnotatedStaticNestedClass.class;
Search search = MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY);
assertThat(search.from(testCase).stream()).isEmpty();
assertThat(search.withEnclosingClasses(Search.never).from(testCase).stream()).isEmpty();
assertThat(search.withEnclosingClasses(ClassUtils::isInnerClass).from(testCase).stream()).isEmpty();
Stream<Class<?>> classes = search.withEnclosingClasses(ClassUtils::isStaticClass)
.from(testCase)
.stream()
.map(MergedAnnotation::getType);
assertThat(classes).containsExactly(Component.class, Indexed.class);
classes = search.withEnclosingClasses(Search.always)
.from(testCase)
.stream()
.map(MergedAnnotation::getType);
assertThat(classes).containsExactly(Component.class, Indexed.class);
}
}
@Test