From 1fe394f11d7ab7f328402a04d13f4d12a89a9f87 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Thu, 24 Mar 2022 15:22:21 +0100 Subject: [PATCH] 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> 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 --- .../core/annotation/AnnotationsScanner.java | 56 ++++++++----- .../core/annotation/MergedAnnotations.java | 75 +++++++++++++++-- .../annotation/TypeMappedAnnotations.java | 20 +++-- .../annotation/AnnotationsScannerTests.java | 20 +++-- .../annotation/MergedAnnotationsTests.java | 82 +++++++++++++++++-- .../context/TestContextAnnotationUtils.java | 15 ++-- 6 files changed, 217 insertions(+), 51 deletions(-) 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); } /**