diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java index 27eae73cd55f..3b5eaaacc12f 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java @@ -23,10 +23,12 @@ 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 @@ private 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 R scan(C context, AnnotatedElement source, SearchStrategy searchStrategy, - AnnotationsProcessor processor) { + Predicate> searchEnclosingClass, AnnotationsProcessor processor) { - R result = process(context, source, searchStrategy, processor); + R result = process(context, source, searchStrategy, searchEnclosingClass, processor); return processor.finish(result); } @Nullable private static R process(C context, AnnotatedElement source, - SearchStrategy searchStrategy, AnnotationsProcessor processor) { + SearchStrategy searchStrategy, Predicate> searchEnclosingClass, + AnnotationsProcessor 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 @@ private static R process(C context, AnnotatedElement source, @Nullable @SuppressWarnings("deprecation") - private static R processClass(C context, Class source, - SearchStrategy searchStrategy, AnnotationsProcessor processor) { + private static R processClass(C context, Class source, SearchStrategy searchStrategy, + Predicate> searchEnclosingClass, AnnotationsProcessor 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 @@ private static R processClassInheritedAnnotations(C context, Class sou SearchStrategy searchStrategy, AnnotationsProcessor processor) { try { - if (isWithoutHierarchy(source, searchStrategy)) { + if (isWithoutHierarchy(source, searchStrategy, Search.never)) { return processElement(context, source, processor); } Annotation[] relevant = null; @@ -161,15 +167,17 @@ private static R processClassInheritedAnnotations(C context, Class sou @Nullable private static R processClassHierarchy(C context, Class source, - AnnotationsProcessor processor, boolean includeInterfaces, boolean includeEnclosing) { + AnnotationsProcessor processor, boolean includeInterfaces, + Predicate> searchEnclosingClass) { return processClassHierarchy(context, new int[] {0}, source, processor, - includeInterfaces, includeEnclosing); + includeInterfaces, searchEnclosingClass); } @Nullable private static R processClassHierarchy(C context, int[] aggregateIndex, Class source, - AnnotationsProcessor processor, boolean includeInterfaces, boolean includeEnclosing) { + AnnotationsProcessor processor, boolean includeInterfaces, + Predicate> searchEnclosingClass) { try { R result = processor.doWithAggregate(context, aggregateIndex[0]); @@ -188,7 +196,7 @@ private static R processClassHierarchy(C context, int[] aggregateIndex, C 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 @@ private static R processClassHierarchy(C context, int[] aggregateIndex, C 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 @@ private static R processClassHierarchy(C context, int[] aggregateIndex, C 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 @@ private static boolean isIgnorable(Class annotationType) { return AnnotationFilter.PLAIN.matches(annotationType); } - static boolean isKnownEmpty(AnnotatedElement source, SearchStrategy searchStrategy) { + static boolean isKnownEmpty(AnnotatedElement source, SearchStrategy searchStrategy, + Predicate> 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 @@ static boolean hasPlainJavaAnnotationsOnly(Class type) { } @SuppressWarnings("deprecation") - private static boolean isWithoutHierarchy(AnnotatedElement source, SearchStrategy searchStrategy) { + private static boolean isWithoutHierarchy(AnnotatedElement source, SearchStrategy searchStrategy, + Predicate> 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; } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java index 3614514820b4..be60a79d10b3 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java @@ -356,11 +356,23 @@ static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStr static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy, RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) { + Predicate> 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> 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 @@ static Search search(SearchStrategy searchStrategy) { */ static final class Search { + static final Predicate> always = clazz -> true; + + static final Predicate> never = clazz -> false; + + private final SearchStrategy searchStrategy; + private Predicate> searchEnclosingClass = never; + private RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables(); private AnnotationFilter annotationFilter = AnnotationFilter.PLAIN; @@ -511,6 +530,47 @@ private Search(SearchStrategy searchStrategy) { this.searchStrategy = searchStrategy; } + /** + * Configure whether the search algorithm should search on + * {@linkplain Class#getEnclosingClass() enclosing classes}. + *

This feature is disabled by default and is only supported when using + * {@link SearchStrategy#TYPE_HIERARCHY}. + *

Enclosing classes will be recursively searched if the supplied + * {@link Predicate} evaluates to {@code true}. Typically, the predicate + * will be used to differentiate between inner classes and + * {@code static} nested classes. + *

    + *
  • To limit the enclosing class search to inner classes, provide + * {@link org.springframework.util.ClassUtils#isInnerClass(Class) ClassUtils::isInnerClass} + * as the predicate.
  • + *
  • To limit the enclosing class search to static nested classes, provide + * {@link org.springframework.util.ClassUtils#isStaticClass(Class) ClassUtils::isStaticClass} + * as the predicate.
  • + *
  • To force the algorithm to always search enclosing classes, provide + * {@code clazz -> true} as the predicate.
  • + *
  • For any other use case, provide a custom predicate.
  • + *
+ *

WARNING: 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 inner class, 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> 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 Search withAnnotationFilter(AnnotationFilter annotationFilter) { * @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 @@ enum SearchStrategy { /** * Perform a full search of the entire type hierarchy, including * superclasses and implemented interfaces. - *

Superclass annotations do not need to be meta-annotated with - * {@link Inherited @Inherited}. + *

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}. + *

Superclass and enclosing class annotations do not need to be + * meta-annotated with {@link Inherited @Inherited}. */ TYPE_HIERARCHY, diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java index ddd078b25cf3..581a74c10bd6 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java @@ -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 @@ * 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> 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> 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 @@ private TypeMappedAnnotations(@Nullable Object source, Annotation[] annotations, 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 @@ private R scan(C criteria, AnnotationsProcessor processor) { 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> 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, diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java index e848090b1241..fa6eb99a1acf 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java @@ -30,6 +30,7 @@ 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 @@ void typeHierarchyWithEnclosedStrategyOnMethodHierarchyUsesTypeHierarchyScan() { @Test void scanWhenProcessorReturnsFromDoWithAggregateExitsEarly() { - String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class, - SearchStrategy.TYPE_HIERARCHY, new AnnotationsProcessor() { + String result = scan(this, WithSingleSuperclass.class, SearchStrategy.TYPE_HIERARCHY, + new AnnotationsProcessor() { @Override @Nullable @@ -471,8 +472,7 @@ public String doWithAnnotations(Object context, int aggregateIndex, @Test void scanWhenProcessorReturnsFromDoWithAnnotationsExitsEarly() { List 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 @@ void scanWhenProcessorReturnsFromDoWithAnnotationsExitsEarly() { @Test void scanWhenProcessorHasFinishMethodUsesFinishResult() { - String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class, - SearchStrategy.TYPE_HIERARCHY, new AnnotationsProcessor() { + String result = scan(this, WithSingleSuperclass.class, SearchStrategy.TYPE_HIERARCHY, + new AnnotationsProcessor() { @Override @Nullable @@ -510,7 +510,7 @@ private Method methodFrom(Class type) { private Stream scan(AnnotatedElement element, SearchStrategy searchStrategy) { List 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 @@ private Stream scan(AnnotatedElement element, SearchStrategy searchStrat return results.stream(); } + private static R scan(C context, AnnotatedElement source, SearchStrategy searchStrategy, + AnnotationsProcessor processor) { + + return AnnotationsScanner.scan(context, source, searchStrategy, Search.never, processor); + } + private void trackIndexedAnnotations(int aggregateIndex, Annotation[] annotations, List results) { Arrays.stream(annotations) .filter(Objects::nonNull) diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java index 2daaf981bfbd..143312b0b631 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsTests.java @@ -89,20 +89,30 @@ void preconditions() { .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> classes = MergedAnnotations.search(SearchStrategy.DIRECT) .from(TransactionalComponent.class) .stream() @@ -111,7 +121,7 @@ void searchOnClassWithDefaultAnnotationFilterAndRepeatableContainers() { } @Test - void searchOnClassWithCustomAnnotationFilter() { + void searchFromClassWithCustomAnnotationFilter() { Stream> classes = MergedAnnotations.search(SearchStrategy.DIRECT) .withAnnotationFilter(annotationName -> annotationName.endsWith("Indexed")) .from(TransactionalComponent.class) @@ -121,19 +131,79 @@ void searchOnClassWithCustomAnnotationFilter() { } @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> 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> 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 diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java index 4d8550e6f29d..7b6aaaee129d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java @@ -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 annotationType) { - return (findMergedAnnotation(clazz, annotationType) != null); + return MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY) + .withEnclosingClasses(TestContextAnnotationUtils::searchEnclosingClass) + .from(clazz) + .isPresent(annotationType); } /** @@ -125,9 +128,11 @@ public static T findMergedAnnotation(Class clazz, Clas private static T findMergedAnnotation(Class clazz, Class annotationType, Predicate> searchEnclosingClass) { - AnnotationDescriptor 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); } /**