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 4a041a9ab5..05737a5a9a 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 @@ -123,10 +123,12 @@ abstract class AnnotationsScanner { case INHERITED_ANNOTATIONS: return processClassInheritedAnnotations(context, source, processor, classFilter); case SUPERCLASS: - return processClassHierarchy(context, new int[] {0}, source, processor, classFilter, false); + return processClassHierarchy(context, source, processor, classFilter, false, false); case EXHAUSTIVE: case TYPE_HIERARCHY: - return processClassHierarchy(context, new int[] {0}, source, processor, classFilter, true); + return processClassHierarchy(context, source, processor, classFilter, true, false); + case TYPE_HIERARCHY_AND_ENCLOSING_CLASSES: + return processClassHierarchy(context, source, processor, classFilter, true, true); } throw new IllegalStateException("Unsupported search strategy " + searchStrategy); } @@ -184,10 +186,22 @@ abstract class AnnotationsScanner { return null; } + @Nullable + private static R processClassHierarchy(C context, Class source, + AnnotationsProcessor processor, + @Nullable BiPredicate> classFilter, boolean includeInterfaces, + boolean includeEnclosing) { + + int[] aggregateIndex = new int[] { 0 }; + return processClassHierarchy(context, aggregateIndex, source, processor, + classFilter, includeInterfaces, includeEnclosing); + } + @Nullable private static R processClassHierarchy(C context, int[] aggregateIndex, Class source, AnnotationsProcessor processor, - @Nullable BiPredicate> classFilter, boolean includeInterfaces) { + @Nullable BiPredicate> classFilter, boolean includeInterfaces, + boolean includeEnclosing) { R result = processor.doWithAggregate(context, aggregateIndex[0]); if (result != null) { @@ -205,7 +219,7 @@ abstract class AnnotationsScanner { if (includeInterfaces) { for (Class interfaceType : source.getInterfaces()) { R interfacesResult = processClassHierarchy(context, aggregateIndex, - interfaceType, processor, classFilter, true); + interfaceType, processor, classFilter, true, includeEnclosing); if (interfacesResult != null) { return interfacesResult; } @@ -214,11 +228,21 @@ abstract class AnnotationsScanner { Class superclass = source.getSuperclass(); if (superclass != Object.class && superclass != null) { R superclassResult = processClassHierarchy(context, aggregateIndex, - superclass, processor, classFilter, includeInterfaces); + superclass, processor, classFilter, includeInterfaces, + includeEnclosing); if (superclassResult != null) { return superclassResult; } } + Class enclosingClass = source.getEnclosingClass(); + if (includeEnclosing && enclosingClass != null) { + R enclosingResult = processClassHierarchy(context, aggregateIndex, + enclosingClass, processor, classFilter, includeInterfaces, + includeEnclosing); + if (enclosingResult != null) { + return enclosingResult; + } + } return null; } @@ -237,6 +261,7 @@ abstract class AnnotationsScanner { processor, classFilter, source, false); case EXHAUSTIVE: case TYPE_HIERARCHY: + case TYPE_HIERARCHY_AND_ENCLOSING_CLASSES: return processMethodHierarchy(context, new int[] {0}, source.getDeclaringClass(), processor, classFilter, source, 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 e2a96cb7c9..fbfb4146f8 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 @@ -434,12 +434,22 @@ public interface MergedAnnotations extends Iterable EXHAUSTIVE, /** - * Perform a full search of the entire type hierarchy , including + * 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}. */ - TYPE_HIERARCHY + TYPE_HIERARCHY, + /** + * Perform a full search of the entire type hierarchy on the source + * and any enclosing classes. This strategy is similar to + * {@link #TYPE_HIERARCHY} except that {@link Class#getEnclosingClass() + * enclosing classes} are also searched. Superclass annotations do not + * need to be meta-annotated with {@link Inherited @Inherited}. When + * searching a {@link Method} source, this strategy is identical to + * {@link #TYPE_HIERARCHY}. + */ + TYPE_HIERARCHY_AND_ENCLOSING_CLASSES } } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationEnclosingClassSample.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationEnclosingClassSample.java new file mode 100644 index 0000000000..7db91cfbe9 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationEnclosingClassSample.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2019 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.annotation; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Example class used to test {@link AnnotationsScanner} with enclosing classes. + * + * @author Phillip Webb + * @since 5.2 + */ +@AnnotationEnclosingClassSample.EnclosedOne +public class AnnotationEnclosingClassSample { + + @EnclosedTwo + public static class EnclosedStatic { + + @EnclosedThree + public static class EnclosedStaticStatic { + + } + + } + + @EnclosedTwo + public class EnclosedInner { + + @EnclosedThree + public class EnclosedInnerInner { + + } + + } + + @Retention(RetentionPolicy.RUNTIME) + public static @interface EnclosedOne { + + } + + @Retention(RetentionPolicy.RUNTIME) + public static @interface EnclosedTwo { + + } + + @Retention(RetentionPolicy.RUNTIME) + public static @interface EnclosedThree { + + } + +} 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 e57341e4a0..96df294216 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 @@ -454,6 +454,29 @@ public class AnnotationsScannerTests { "0:TestAnnotation1"); } + @Test + public void typeHierarchyWithEnclosedStrategyOnEnclosedStaticClassScansAnnotations() { + Class source = AnnotationEnclosingClassSample.EnclosedStatic.EnclosedStaticStatic.class; + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)) + .containsExactly("0:EnclosedThree", "1:EnclosedTwo", "2:EnclosedOne"); + } + + @Test + public void typeHierarchyWithEnclosedStrategyOnEnclosedInnerClassScansAnnotations() { + Class source = AnnotationEnclosingClassSample.EnclosedInner.EnclosedInnerInner.class; + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)) + .containsExactly("0:EnclosedThree", "1:EnclosedTwo", "2:EnclosedOne"); + } + + @Test + public void typeHierarchyWithEnclosedStrategyOnMethodHierarchyUsesTypeHierarchyScan() { + Method source = methodFrom(WithHierarchy.class); + assertThat(scan(source, SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES)).containsExactly( + "0:TestAnnotation1", "1:TestAnnotation5", "1:TestInheritedAnnotation5", + "2:TestAnnotation6", "3:TestAnnotation2", "3:TestInheritedAnnotation2", + "4:TestAnnotation3", "5:TestAnnotation4"); + } + @Test public void scanWhenProcessorReturnsFromDoWithAggregateExitsEarly() { String result = AnnotationsScanner.scan(this, WithSingleSuperclass.class,