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

@ -23,10 +23,12 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Predicate;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotations.Search;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.lang.Nullable;
import org.springframework.util.ConcurrentReferenceHashMap;
@ -67,23 +69,27 @@ abstract class AnnotationsScanner {
* processor
* @param source the source element to scan
* @param searchStrategy the search strategy to use
* @param searchEnclosingClass a predicate which evaluates to {@code true}
* if a search should be performed on the enclosing class of the class
* supplied to the predicate
* @param processor the processor that receives the annotations
* @return the result of {@link AnnotationsProcessor#finish(Object)}
*/
@Nullable
static <C, R> R scan(C context, AnnotatedElement source, SearchStrategy searchStrategy,
AnnotationsProcessor<C, R> processor) {
Predicate<Class<?>> searchEnclosingClass, AnnotationsProcessor<C, R> processor) {
R result = process(context, source, searchStrategy, processor);
R result = process(context, source, searchStrategy, searchEnclosingClass, processor);
return processor.finish(result);
}
@Nullable
private static <C, R> R process(C context, AnnotatedElement source,
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor) {
SearchStrategy searchStrategy, Predicate<Class<?>> searchEnclosingClass,
AnnotationsProcessor<C, R> processor) {
if (source instanceof Class<?> clazz) {
return processClass(context, clazz, searchStrategy, processor);
return processClass(context, clazz, searchStrategy, searchEnclosingClass, processor);
}
if (source instanceof Method method) {
return processMethod(context, method, searchStrategy, processor);
@ -93,15 +99,15 @@ abstract class AnnotationsScanner {
@Nullable
@SuppressWarnings("deprecation")
private static <C, R> R processClass(C context, Class<?> source,
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor) {
private static <C, R> R processClass(C context, Class<?> source, SearchStrategy searchStrategy,
Predicate<Class<?>> searchEnclosingClass, AnnotationsProcessor<C, R> processor) {
return switch (searchStrategy) {
case DIRECT -> processElement(context, source, processor);
case INHERITED_ANNOTATIONS -> processClassInheritedAnnotations(context, source, searchStrategy, processor);
case SUPERCLASS -> processClassHierarchy(context, source, processor, false, false);
case TYPE_HIERARCHY -> processClassHierarchy(context, source, processor, true, false);
case TYPE_HIERARCHY_AND_ENCLOSING_CLASSES -> processClassHierarchy(context, source, processor, true, true);
case SUPERCLASS -> processClassHierarchy(context, source, processor, false, Search.never);
case TYPE_HIERARCHY -> processClassHierarchy(context, source, processor, true, searchEnclosingClass);
case TYPE_HIERARCHY_AND_ENCLOSING_CLASSES -> processClassHierarchy(context, source, processor, true, Search.always);
};
}
@ -110,7 +116,7 @@ abstract class AnnotationsScanner {
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor) {
try {
if (isWithoutHierarchy(source, searchStrategy)) {
if (isWithoutHierarchy(source, searchStrategy, Search.never)) {
return processElement(context, source, processor);
}
Annotation[] relevant = null;
@ -161,15 +167,17 @@ abstract class AnnotationsScanner {
@Nullable
private static <C, R> R processClassHierarchy(C context, Class<?> source,
AnnotationsProcessor<C, R> processor, boolean includeInterfaces, boolean includeEnclosing) {
AnnotationsProcessor<C, R> processor, boolean includeInterfaces,
Predicate<Class<?>> searchEnclosingClass) {
return processClassHierarchy(context, new int[] {0}, source, processor,
includeInterfaces, includeEnclosing);
includeInterfaces, searchEnclosingClass);
}
@Nullable
private static <C, R> R processClassHierarchy(C context, int[] aggregateIndex, Class<?> source,
AnnotationsProcessor<C, R> processor, boolean includeInterfaces, boolean includeEnclosing) {
AnnotationsProcessor<C, R> processor, boolean includeInterfaces,
Predicate<Class<?>> searchEnclosingClass) {
try {
R result = processor.doWithAggregate(context, aggregateIndex[0]);
@ -188,7 +196,7 @@ abstract class AnnotationsScanner {
if (includeInterfaces) {
for (Class<?> interfaceType : source.getInterfaces()) {
R interfacesResult = processClassHierarchy(context, aggregateIndex,
interfaceType, processor, true, includeEnclosing);
interfaceType, processor, true, searchEnclosingClass);
if (interfacesResult != null) {
return interfacesResult;
}
@ -197,12 +205,12 @@ abstract class AnnotationsScanner {
Class<?> superclass = source.getSuperclass();
if (superclass != Object.class && superclass != null) {
R superclassResult = processClassHierarchy(context, aggregateIndex,
superclass, processor, includeInterfaces, includeEnclosing);
superclass, processor, includeInterfaces, searchEnclosingClass);
if (superclassResult != null) {
return superclassResult;
}
}
if (includeEnclosing) {
if (searchEnclosingClass.test(source)) {
// Since merely attempting to load the enclosing class may result in
// automatic loading of sibling nested classes that in turn results
// in an exception such as NoClassDefFoundError, we wrap the following
@ -212,7 +220,7 @@ abstract class AnnotationsScanner {
Class<?> enclosingClass = source.getEnclosingClass();
if (enclosingClass != null) {
R enclosingResult = processClassHierarchy(context, aggregateIndex,
enclosingClass, processor, includeInterfaces, true);
enclosingClass, processor, includeInterfaces, searchEnclosingClass);
if (enclosingResult != null) {
return enclosingResult;
}
@ -472,11 +480,13 @@ abstract class AnnotationsScanner {
return AnnotationFilter.PLAIN.matches(annotationType);
}
static boolean isKnownEmpty(AnnotatedElement source, SearchStrategy searchStrategy) {
static boolean isKnownEmpty(AnnotatedElement source, SearchStrategy searchStrategy,
Predicate<Class<?>> searchEnclosingClass) {
if (hasPlainJavaAnnotationsOnly(source)) {
return true;
}
if (searchStrategy == SearchStrategy.DIRECT || isWithoutHierarchy(source, searchStrategy)) {
if (searchStrategy == SearchStrategy.DIRECT || isWithoutHierarchy(source, searchStrategy, searchEnclosingClass)) {
if (source instanceof Method method && method.isBridge()) {
return false;
}
@ -502,19 +512,21 @@ abstract class AnnotationsScanner {
}
@SuppressWarnings("deprecation")
private static boolean isWithoutHierarchy(AnnotatedElement source, SearchStrategy searchStrategy) {
private static boolean isWithoutHierarchy(AnnotatedElement source, SearchStrategy searchStrategy,
Predicate<Class<?>> searchEnclosingClass) {
if (source == Object.class) {
return true;
}
if (source instanceof Class<?> sourceClass) {
boolean noSuperTypes = (sourceClass.getSuperclass() == Object.class &&
sourceClass.getInterfaces().length == 0);
return (searchStrategy == SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES ? noSuperTypes &&
return (searchEnclosingClass.test(sourceClass) ? noSuperTypes &&
sourceClass.getEnclosingClass() == null : noSuperTypes);
}
if (source instanceof Method sourceMethod) {
return (Modifier.isPrivate(sourceMethod.getModifiers()) ||
isWithoutHierarchy(sourceMethod.getDeclaringClass(), searchStrategy));
isWithoutHierarchy(sourceMethod.getDeclaringClass(), searchStrategy, searchEnclosingClass));
}
return true;
}

View File

@ -356,11 +356,23 @@ public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>
static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy,
RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) {
Predicate<Class<?>> searchEnclosingClass =
(searchStrategy == SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES ?
Search.always : Search.never);
return from(element, searchStrategy, searchEnclosingClass, repeatableContainers, annotationFilter);
}
private static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy,
Predicate<Class<?>> searchEnclosingClass, RepeatableContainers repeatableContainers,
AnnotationFilter annotationFilter) {
Assert.notNull(element, "AnnotatedElement must not be null");
Assert.notNull(searchStrategy, "SearchStrategy must not be null");
Assert.notNull(searchEnclosingClass, "Predicate must not be null");
Assert.notNull(repeatableContainers, "RepeatableContainers must not be null");
Assert.notNull(annotationFilter, "AnnotationFilter must not be null");
return TypeMappedAnnotations.from(element, searchStrategy, repeatableContainers, annotationFilter);
return TypeMappedAnnotations.from(element, searchStrategy, searchEnclosingClass,
repeatableContainers, annotationFilter);
}
/**
@ -500,8 +512,15 @@ public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>
*/
static final class Search {
static final Predicate<Class<?>> always = clazz -> true;
static final Predicate<Class<?>> never = clazz -> false;
private final SearchStrategy searchStrategy;
private Predicate<Class<?>> searchEnclosingClass = never;
private RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables();
private AnnotationFilter annotationFilter = AnnotationFilter.PLAIN;
@ -511,6 +530,47 @@ public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>
this.searchStrategy = searchStrategy;
}
/**
* Configure whether the search algorithm should search on
* {@linkplain Class#getEnclosingClass() enclosing classes}.
* <p>This feature is disabled by default and is only supported when using
* {@link SearchStrategy#TYPE_HIERARCHY}.
* <p>Enclosing classes will be recursively searched if the supplied
* {@link Predicate} evaluates to {@code true}. Typically, the predicate
* will be used to differentiate between <em>inner classes</em> and
* {@code static} nested classes.
* <ul>
* <li>To limit the enclosing class search to inner classes, provide
* {@link org.springframework.util.ClassUtils#isInnerClass(Class) ClassUtils::isInnerClass}
* as the predicate.</li>
* <li>To limit the enclosing class search to static nested classes, provide
* {@link org.springframework.util.ClassUtils#isStaticClass(Class) ClassUtils::isStaticClass}
* as the predicate.</li>
* <li>To force the algorithm to always search enclosing classes, provide
* {@code clazz -> true} as the predicate.</li>
* <li>For any other use case, provide a custom predicate.</li>
* </ul>
* <p><strong>WARNING:</strong> if the supplied predicate always evaluates
* to {@code true}, the algorithm will search recursively for annotations
* on an enclosing class for any source type, regardless whether the source
* type is an <em>inner class</em>, a {@code static} nested class, or a
* nested interface. Thus, it may find more annotations than you would expect.
* @param searchEnclosingClass a predicate which evaluates to {@code true}
* if a search should be performed on the enclosing class of the class
* supplied to the predicate
* @return this {@code Search} instance for chained method invocations
* @see SearchStrategy#TYPE_HIERARCHY
* @see #withRepeatableContainers(RepeatableContainers)
* @see #withAnnotationFilter(AnnotationFilter)
* @see #from(AnnotatedElement)
*/
public Search withEnclosingClasses(Predicate<Class<?>> searchEnclosingClass) {
Assert.notNull(searchEnclosingClass, "Predicate must not be null");
Assert.state(this.searchStrategy == SearchStrategy.TYPE_HIERARCHY,
"A custom 'searchEnclosingClass' predicate can only be combined with SearchStrategy.TYPE_HIERARCHY");
this.searchEnclosingClass = searchEnclosingClass;
return this;
}
/**
* Configure the {@link RepeatableContainers} to use.
@ -550,13 +610,14 @@ public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>
* @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 #withEnclosingClasses(Predicate)
* @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);
return MergedAnnotations.from(element, this.searchStrategy, this.searchEnclosingClass,
this.repeatableContainers, this.annotationFilter);
}
}
@ -600,8 +661,12 @@ public interface MergedAnnotations extends Iterable<MergedAnnotation<Annotation>
/**
* Perform a full search of the entire type hierarchy, including
* superclasses and implemented interfaces.
* <p>Superclass annotations do not need to be meta-annotated with
* {@link Inherited @Inherited}.
* <p>When combined with {@link Search#withEnclosingClasses(Predicate)},
* {@linkplain Class#getEnclosingClass() enclosing classes} will also be
* recursively searched if the supplied {@link Predicate} evaluates to
* {@code true}.
* <p>Superclass and enclosing class annotations do not need to be
* meta-annotated with {@link Inherited @Inherited}.
*/
TYPE_HIERARCHY,

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -36,6 +36,7 @@ import org.springframework.lang.Nullable;
* annotations and meta-annotations using {@link AnnotationTypeMappings}.
*
* @author Phillip Webb
* @author Sam Brannen
* @since 5.2
*/
final class TypeMappedAnnotations implements MergedAnnotations {
@ -56,6 +57,8 @@ final class TypeMappedAnnotations implements MergedAnnotations {
@Nullable
private final SearchStrategy searchStrategy;
private final Predicate<Class<?>> searchEnclosingClass;
@Nullable
private final Annotation[] annotations;
@ -68,11 +71,13 @@ final class TypeMappedAnnotations implements MergedAnnotations {
private TypeMappedAnnotations(AnnotatedElement element, SearchStrategy searchStrategy,
RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) {
Predicate<Class<?>> searchEnclosingClass, RepeatableContainers repeatableContainers,
AnnotationFilter annotationFilter) {
this.source = element;
this.element = element;
this.searchStrategy = searchStrategy;
this.searchEnclosingClass = searchEnclosingClass;
this.annotations = null;
this.repeatableContainers = repeatableContainers;
this.annotationFilter = annotationFilter;
@ -84,6 +89,7 @@ final class TypeMappedAnnotations implements MergedAnnotations {
this.source = source;
this.element = null;
this.searchStrategy = null;
this.searchEnclosingClass = Search.never;
this.annotations = annotations;
this.repeatableContainers = repeatableContainers;
this.annotationFilter = annotationFilter;
@ -239,19 +245,21 @@ final class TypeMappedAnnotations implements MergedAnnotations {
return processor.finish(result);
}
if (this.element != null && this.searchStrategy != null) {
return AnnotationsScanner.scan(criteria, this.element, this.searchStrategy, processor);
return AnnotationsScanner.scan(criteria, this.element, this.searchStrategy,
this.searchEnclosingClass, processor);
}
return null;
}
static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy,
RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) {
Predicate<Class<?>> searchEnclosingClass, RepeatableContainers repeatableContainers,
AnnotationFilter annotationFilter) {
if (AnnotationsScanner.isKnownEmpty(element, searchStrategy)) {
if (AnnotationsScanner.isKnownEmpty(element, searchStrategy, searchEnclosingClass)) {
return NONE;
}
return new TypeMappedAnnotations(element, searchStrategy, repeatableContainers, annotationFilter);
return new TypeMappedAnnotations(element, searchStrategy, searchEnclosingClass, repeatableContainers, annotationFilter);
}
static MergedAnnotations from(@Nullable Object source, Annotation[] annotations,

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

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -91,7 +91,10 @@ public abstract class TestContextAnnotationUtils {
* @see #findMergedAnnotation(Class, Class)
*/
public static boolean hasAnnotation(Class<?> clazz, Class<? extends Annotation> annotationType) {
return (findMergedAnnotation(clazz, annotationType) != null);
return MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
.withEnclosingClasses(TestContextAnnotationUtils::searchEnclosingClass)
.from(clazz)
.isPresent(annotationType);
}
/**
@ -125,9 +128,11 @@ public abstract class TestContextAnnotationUtils {
private static <T extends Annotation> T findMergedAnnotation(Class<?> clazz, Class<T> annotationType,
Predicate<Class<?>> searchEnclosingClass) {
AnnotationDescriptor<T> descriptor =
findAnnotationDescriptor(clazz, annotationType, searchEnclosingClass, new HashSet<>());
return (descriptor != null ? descriptor.getAnnotation() : null);
return MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY)
.withEnclosingClasses(searchEnclosingClass)
.from(clazz)
.get(annotationType)
.synthesize(MergedAnnotation::isPresent).orElse(null);
}
/**