Skip to content

Commit 1fe394f

Browse files
committed
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
1 parent 7161940 commit 1fe394f

File tree

6 files changed

+217
-51
lines changed

6 files changed

+217
-51
lines changed

spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java

+34-22
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
import java.lang.reflect.Modifier;
2424
import java.util.Arrays;
2525
import java.util.Map;
26+
import java.util.function.Predicate;
2627

2728
import org.springframework.core.BridgeMethodResolver;
2829
import org.springframework.core.Ordered;
2930
import org.springframework.core.ResolvableType;
31+
import org.springframework.core.annotation.MergedAnnotations.Search;
3032
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
3133
import org.springframework.lang.Nullable;
3234
import org.springframework.util.ConcurrentReferenceHashMap;
@@ -67,23 +69,27 @@ private AnnotationsScanner() {
6769
* processor
6870
* @param source the source element to scan
6971
* @param searchStrategy the search strategy to use
72+
* @param searchEnclosingClass a predicate which evaluates to {@code true}
73+
* if a search should be performed on the enclosing class of the class
74+
* supplied to the predicate
7075
* @param processor the processor that receives the annotations
7176
* @return the result of {@link AnnotationsProcessor#finish(Object)}
7277
*/
7378
@Nullable
7479
static <C, R> R scan(C context, AnnotatedElement source, SearchStrategy searchStrategy,
75-
AnnotationsProcessor<C, R> processor) {
80+
Predicate<Class<?>> searchEnclosingClass, AnnotationsProcessor<C, R> processor) {
7681

77-
R result = process(context, source, searchStrategy, processor);
82+
R result = process(context, source, searchStrategy, searchEnclosingClass, processor);
7883
return processor.finish(result);
7984
}
8085

8186
@Nullable
8287
private static <C, R> R process(C context, AnnotatedElement source,
83-
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor) {
88+
SearchStrategy searchStrategy, Predicate<Class<?>> searchEnclosingClass,
89+
AnnotationsProcessor<C, R> processor) {
8490

8591
if (source instanceof Class<?> clazz) {
86-
return processClass(context, clazz, searchStrategy, processor);
92+
return processClass(context, clazz, searchStrategy, searchEnclosingClass, processor);
8793
}
8894
if (source instanceof Method method) {
8995
return processMethod(context, method, searchStrategy, processor);
@@ -93,15 +99,15 @@ private static <C, R> R process(C context, AnnotatedElement source,
9399

94100
@Nullable
95101
@SuppressWarnings("deprecation")
96-
private static <C, R> R processClass(C context, Class<?> source,
97-
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor) {
102+
private static <C, R> R processClass(C context, Class<?> source, SearchStrategy searchStrategy,
103+
Predicate<Class<?>> searchEnclosingClass, AnnotationsProcessor<C, R> processor) {
98104

99105
return switch (searchStrategy) {
100106
case DIRECT -> processElement(context, source, processor);
101107
case INHERITED_ANNOTATIONS -> processClassInheritedAnnotations(context, source, searchStrategy, processor);
102-
case SUPERCLASS -> processClassHierarchy(context, source, processor, false, false);
103-
case TYPE_HIERARCHY -> processClassHierarchy(context, source, processor, true, false);
104-
case TYPE_HIERARCHY_AND_ENCLOSING_CLASSES -> processClassHierarchy(context, source, processor, true, true);
108+
case SUPERCLASS -> processClassHierarchy(context, source, processor, false, Search.never);
109+
case TYPE_HIERARCHY -> processClassHierarchy(context, source, processor, true, searchEnclosingClass);
110+
case TYPE_HIERARCHY_AND_ENCLOSING_CLASSES -> processClassHierarchy(context, source, processor, true, Search.always);
105111
};
106112
}
107113

@@ -110,7 +116,7 @@ private static <C, R> R processClassInheritedAnnotations(C context, Class<?> sou
110116
SearchStrategy searchStrategy, AnnotationsProcessor<C, R> processor) {
111117

112118
try {
113-
if (isWithoutHierarchy(source, searchStrategy)) {
119+
if (isWithoutHierarchy(source, searchStrategy, Search.never)) {
114120
return processElement(context, source, processor);
115121
}
116122
Annotation[] relevant = null;
@@ -161,15 +167,17 @@ private static <C, R> R processClassInheritedAnnotations(C context, Class<?> sou
161167

162168
@Nullable
163169
private static <C, R> R processClassHierarchy(C context, Class<?> source,
164-
AnnotationsProcessor<C, R> processor, boolean includeInterfaces, boolean includeEnclosing) {
170+
AnnotationsProcessor<C, R> processor, boolean includeInterfaces,
171+
Predicate<Class<?>> searchEnclosingClass) {
165172

166173
return processClassHierarchy(context, new int[] {0}, source, processor,
167-
includeInterfaces, includeEnclosing);
174+
includeInterfaces, searchEnclosingClass);
168175
}
169176

170177
@Nullable
171178
private static <C, R> R processClassHierarchy(C context, int[] aggregateIndex, Class<?> source,
172-
AnnotationsProcessor<C, R> processor, boolean includeInterfaces, boolean includeEnclosing) {
179+
AnnotationsProcessor<C, R> processor, boolean includeInterfaces,
180+
Predicate<Class<?>> searchEnclosingClass) {
173181

174182
try {
175183
R result = processor.doWithAggregate(context, aggregateIndex[0]);
@@ -188,7 +196,7 @@ private static <C, R> R processClassHierarchy(C context, int[] aggregateIndex, C
188196
if (includeInterfaces) {
189197
for (Class<?> interfaceType : source.getInterfaces()) {
190198
R interfacesResult = processClassHierarchy(context, aggregateIndex,
191-
interfaceType, processor, true, includeEnclosing);
199+
interfaceType, processor, true, searchEnclosingClass);
192200
if (interfacesResult != null) {
193201
return interfacesResult;
194202
}
@@ -197,12 +205,12 @@ private static <C, R> R processClassHierarchy(C context, int[] aggregateIndex, C
197205
Class<?> superclass = source.getSuperclass();
198206
if (superclass != Object.class && superclass != null) {
199207
R superclassResult = processClassHierarchy(context, aggregateIndex,
200-
superclass, processor, includeInterfaces, includeEnclosing);
208+
superclass, processor, includeInterfaces, searchEnclosingClass);
201209
if (superclassResult != null) {
202210
return superclassResult;
203211
}
204212
}
205-
if (includeEnclosing) {
213+
if (searchEnclosingClass.test(source)) {
206214
// Since merely attempting to load the enclosing class may result in
207215
// automatic loading of sibling nested classes that in turn results
208216
// in an exception such as NoClassDefFoundError, we wrap the following
@@ -212,7 +220,7 @@ private static <C, R> R processClassHierarchy(C context, int[] aggregateIndex, C
212220
Class<?> enclosingClass = source.getEnclosingClass();
213221
if (enclosingClass != null) {
214222
R enclosingResult = processClassHierarchy(context, aggregateIndex,
215-
enclosingClass, processor, includeInterfaces, true);
223+
enclosingClass, processor, includeInterfaces, searchEnclosingClass);
216224
if (enclosingResult != null) {
217225
return enclosingResult;
218226
}
@@ -472,11 +480,13 @@ private static boolean isIgnorable(Class<?> annotationType) {
472480
return AnnotationFilter.PLAIN.matches(annotationType);
473481
}
474482

475-
static boolean isKnownEmpty(AnnotatedElement source, SearchStrategy searchStrategy) {
483+
static boolean isKnownEmpty(AnnotatedElement source, SearchStrategy searchStrategy,
484+
Predicate<Class<?>> searchEnclosingClass) {
485+
476486
if (hasPlainJavaAnnotationsOnly(source)) {
477487
return true;
478488
}
479-
if (searchStrategy == SearchStrategy.DIRECT || isWithoutHierarchy(source, searchStrategy)) {
489+
if (searchStrategy == SearchStrategy.DIRECT || isWithoutHierarchy(source, searchStrategy, searchEnclosingClass)) {
480490
if (source instanceof Method method && method.isBridge()) {
481491
return false;
482492
}
@@ -502,19 +512,21 @@ static boolean hasPlainJavaAnnotationsOnly(Class<?> type) {
502512
}
503513

504514
@SuppressWarnings("deprecation")
505-
private static boolean isWithoutHierarchy(AnnotatedElement source, SearchStrategy searchStrategy) {
515+
private static boolean isWithoutHierarchy(AnnotatedElement source, SearchStrategy searchStrategy,
516+
Predicate<Class<?>> searchEnclosingClass) {
517+
506518
if (source == Object.class) {
507519
return true;
508520
}
509521
if (source instanceof Class<?> sourceClass) {
510522
boolean noSuperTypes = (sourceClass.getSuperclass() == Object.class &&
511523
sourceClass.getInterfaces().length == 0);
512-
return (searchStrategy == SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES ? noSuperTypes &&
524+
return (searchEnclosingClass.test(sourceClass) ? noSuperTypes &&
513525
sourceClass.getEnclosingClass() == null : noSuperTypes);
514526
}
515527
if (source instanceof Method sourceMethod) {
516528
return (Modifier.isPrivate(sourceMethod.getModifiers()) ||
517-
isWithoutHierarchy(sourceMethod.getDeclaringClass(), searchStrategy));
529+
isWithoutHierarchy(sourceMethod.getDeclaringClass(), searchStrategy, searchEnclosingClass));
518530
}
519531
return true;
520532
}

spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java

+70-5
Original file line numberDiff line numberDiff line change
@@ -356,11 +356,23 @@ static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStr
356356
static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy,
357357
RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) {
358358

359+
Predicate<Class<?>> searchEnclosingClass =
360+
(searchStrategy == SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES ?
361+
Search.always : Search.never);
362+
return from(element, searchStrategy, searchEnclosingClass, repeatableContainers, annotationFilter);
363+
}
364+
365+
private static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy,
366+
Predicate<Class<?>> searchEnclosingClass, RepeatableContainers repeatableContainers,
367+
AnnotationFilter annotationFilter) {
368+
359369
Assert.notNull(element, "AnnotatedElement must not be null");
360370
Assert.notNull(searchStrategy, "SearchStrategy must not be null");
371+
Assert.notNull(searchEnclosingClass, "Predicate must not be null");
361372
Assert.notNull(repeatableContainers, "RepeatableContainers must not be null");
362373
Assert.notNull(annotationFilter, "AnnotationFilter must not be null");
363-
return TypeMappedAnnotations.from(element, searchStrategy, repeatableContainers, annotationFilter);
374+
return TypeMappedAnnotations.from(element, searchStrategy, searchEnclosingClass,
375+
repeatableContainers, annotationFilter);
364376
}
365377

366378
/**
@@ -500,8 +512,15 @@ static Search search(SearchStrategy searchStrategy) {
500512
*/
501513
static final class Search {
502514

515+
static final Predicate<Class<?>> always = clazz -> true;
516+
517+
static final Predicate<Class<?>> never = clazz -> false;
518+
519+
503520
private final SearchStrategy searchStrategy;
504521

522+
private Predicate<Class<?>> searchEnclosingClass = never;
523+
505524
private RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables();
506525

507526
private AnnotationFilter annotationFilter = AnnotationFilter.PLAIN;
@@ -511,6 +530,47 @@ private Search(SearchStrategy searchStrategy) {
511530
this.searchStrategy = searchStrategy;
512531
}
513532

533+
/**
534+
* Configure whether the search algorithm should search on
535+
* {@linkplain Class#getEnclosingClass() enclosing classes}.
536+
* <p>This feature is disabled by default and is only supported when using
537+
* {@link SearchStrategy#TYPE_HIERARCHY}.
538+
* <p>Enclosing classes will be recursively searched if the supplied
539+
* {@link Predicate} evaluates to {@code true}. Typically, the predicate
540+
* will be used to differentiate between <em>inner classes</em> and
541+
* {@code static} nested classes.
542+
* <ul>
543+
* <li>To limit the enclosing class search to inner classes, provide
544+
* {@link org.springframework.util.ClassUtils#isInnerClass(Class) ClassUtils::isInnerClass}
545+
* as the predicate.</li>
546+
* <li>To limit the enclosing class search to static nested classes, provide
547+
* {@link org.springframework.util.ClassUtils#isStaticClass(Class) ClassUtils::isStaticClass}
548+
* as the predicate.</li>
549+
* <li>To force the algorithm to always search enclosing classes, provide
550+
* {@code clazz -> true} as the predicate.</li>
551+
* <li>For any other use case, provide a custom predicate.</li>
552+
* </ul>
553+
* <p><strong>WARNING:</strong> if the supplied predicate always evaluates
554+
* to {@code true}, the algorithm will search recursively for annotations
555+
* on an enclosing class for any source type, regardless whether the source
556+
* type is an <em>inner class</em>, a {@code static} nested class, or a
557+
* nested interface. Thus, it may find more annotations than you would expect.
558+
* @param searchEnclosingClass a predicate which evaluates to {@code true}
559+
* if a search should be performed on the enclosing class of the class
560+
* supplied to the predicate
561+
* @return this {@code Search} instance for chained method invocations
562+
* @see SearchStrategy#TYPE_HIERARCHY
563+
* @see #withRepeatableContainers(RepeatableContainers)
564+
* @see #withAnnotationFilter(AnnotationFilter)
565+
* @see #from(AnnotatedElement)
566+
*/
567+
public Search withEnclosingClasses(Predicate<Class<?>> searchEnclosingClass) {
568+
Assert.notNull(searchEnclosingClass, "Predicate must not be null");
569+
Assert.state(this.searchStrategy == SearchStrategy.TYPE_HIERARCHY,
570+
"A custom 'searchEnclosingClass' predicate can only be combined with SearchStrategy.TYPE_HIERARCHY");
571+
this.searchEnclosingClass = searchEnclosingClass;
572+
return this;
573+
}
514574

515575
/**
516576
* Configure the {@link RepeatableContainers} to use.
@@ -550,13 +610,14 @@ public Search withAnnotationFilter(AnnotationFilter annotationFilter) {
550610
* @return a new {@link MergedAnnotations} instance containing all
551611
* annotations and meta-annotations from the specified element and,
552612
* depending on the {@link SearchStrategy}, related inherited elements
613+
* @see #withEnclosingClasses(Predicate)
553614
* @see #withRepeatableContainers(RepeatableContainers)
554615
* @see #withAnnotationFilter(AnnotationFilter)
555616
* @see MergedAnnotations#from(AnnotatedElement, SearchStrategy, RepeatableContainers, AnnotationFilter)
556617
*/
557618
public MergedAnnotations from(AnnotatedElement element) {
558-
return MergedAnnotations.from(element, this.searchStrategy, this.repeatableContainers,
559-
this.annotationFilter);
619+
return MergedAnnotations.from(element, this.searchStrategy, this.searchEnclosingClass,
620+
this.repeatableContainers, this.annotationFilter);
560621
}
561622

562623
}
@@ -600,8 +661,12 @@ enum SearchStrategy {
600661
/**
601662
* Perform a full search of the entire type hierarchy, including
602663
* superclasses and implemented interfaces.
603-
* <p>Superclass annotations do not need to be meta-annotated with
604-
* {@link Inherited @Inherited}.
664+
* <p>When combined with {@link Search#withEnclosingClasses(Predicate)},
665+
* {@linkplain Class#getEnclosingClass() enclosing classes} will also be
666+
* recursively searched if the supplied {@link Predicate} evaluates to
667+
* {@code true}.
668+
* <p>Superclass and enclosing class annotations do not need to be
669+
* meta-annotated with {@link Inherited @Inherited}.
605670
*/
606671
TYPE_HIERARCHY,
607672

spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@
3636
* annotations and meta-annotations using {@link AnnotationTypeMappings}.
3737
*
3838
* @author Phillip Webb
39+
* @author Sam Brannen
3940
* @since 5.2
4041
*/
4142
final class TypeMappedAnnotations implements MergedAnnotations {
@@ -56,6 +57,8 @@ final class TypeMappedAnnotations implements MergedAnnotations {
5657
@Nullable
5758
private final SearchStrategy searchStrategy;
5859

60+
private final Predicate<Class<?>> searchEnclosingClass;
61+
5962
@Nullable
6063
private final Annotation[] annotations;
6164

@@ -68,11 +71,13 @@ final class TypeMappedAnnotations implements MergedAnnotations {
6871

6972

7073
private TypeMappedAnnotations(AnnotatedElement element, SearchStrategy searchStrategy,
71-
RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) {
74+
Predicate<Class<?>> searchEnclosingClass, RepeatableContainers repeatableContainers,
75+
AnnotationFilter annotationFilter) {
7276

7377
this.source = element;
7478
this.element = element;
7579
this.searchStrategy = searchStrategy;
80+
this.searchEnclosingClass = searchEnclosingClass;
7681
this.annotations = null;
7782
this.repeatableContainers = repeatableContainers;
7883
this.annotationFilter = annotationFilter;
@@ -84,6 +89,7 @@ private TypeMappedAnnotations(@Nullable Object source, Annotation[] annotations,
8489
this.source = source;
8590
this.element = null;
8691
this.searchStrategy = null;
92+
this.searchEnclosingClass = Search.never;
8793
this.annotations = annotations;
8894
this.repeatableContainers = repeatableContainers;
8995
this.annotationFilter = annotationFilter;
@@ -239,19 +245,21 @@ private <C, R> R scan(C criteria, AnnotationsProcessor<C, R> processor) {
239245
return processor.finish(result);
240246
}
241247
if (this.element != null && this.searchStrategy != null) {
242-
return AnnotationsScanner.scan(criteria, this.element, this.searchStrategy, processor);
248+
return AnnotationsScanner.scan(criteria, this.element, this.searchStrategy,
249+
this.searchEnclosingClass, processor);
243250
}
244251
return null;
245252
}
246253

247254

248255
static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStrategy,
249-
RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) {
256+
Predicate<Class<?>> searchEnclosingClass, RepeatableContainers repeatableContainers,
257+
AnnotationFilter annotationFilter) {
250258

251-
if (AnnotationsScanner.isKnownEmpty(element, searchStrategy)) {
259+
if (AnnotationsScanner.isKnownEmpty(element, searchStrategy, searchEnclosingClass)) {
252260
return NONE;
253261
}
254-
return new TypeMappedAnnotations(element, searchStrategy, repeatableContainers, annotationFilter);
262+
return new TypeMappedAnnotations(element, searchStrategy, searchEnclosingClass, repeatableContainers, annotationFilter);
255263
}
256264

257265
static MergedAnnotations from(@Nullable Object source, Annotation[] annotations,

0 commit comments

Comments
 (0)