Skip to content

Commit

Permalink
Introduce predicate for searching enclosing classes in MergedAnnotations
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sbrannen committed Mar 24, 2022
1 parent 7161940 commit 1fe394f
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 51 deletions.
Expand Up @@ -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;
Expand Down Expand Up @@ -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 <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);
Expand All @@ -93,15 +99,15 @@ private static <C, R> R process(C context, AnnotatedElement source,

@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);
};
}

Expand All @@ -110,7 +116,7 @@ private static <C, R> R processClassInheritedAnnotations(C context, Class<?> sou
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;
Expand Down Expand Up @@ -161,15 +167,17 @@ private static <C, R> R processClassInheritedAnnotations(C context, Class<?> sou

@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]);
Expand All @@ -188,7 +196,7 @@ private static <C, R> 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;
}
Expand All @@ -197,12 +205,12 @@ private static <C, R> 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
Expand All @@ -212,7 +220,7 @@ private static <C, R> 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;
}
Expand Down Expand Up @@ -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<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;
}
Expand All @@ -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<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;
}
Expand Down
Expand Up @@ -356,11 +356,23 @@ static MergedAnnotations from(AnnotatedElement element, SearchStrategy searchStr
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);
}

/**
Expand Down Expand Up @@ -500,8 +512,15 @@ static Search search(SearchStrategy searchStrategy) {
*/
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;
Expand All @@ -511,6 +530,47 @@ private Search(SearchStrategy searchStrategy) {
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.
Expand Down Expand Up @@ -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);
}

}
Expand Down Expand Up @@ -600,8 +661,12 @@ enum SearchStrategy {
/**
* 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,

Expand Down
@@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -56,6 +57,8 @@ final class TypeMappedAnnotations implements MergedAnnotations {
@Nullable
private final SearchStrategy searchStrategy;

private final Predicate<Class<?>> searchEnclosingClass;

@Nullable
private final Annotation[] annotations;

Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -239,19 +245,21 @@ private <C, R> R scan(C criteria, AnnotationsProcessor<C, R> 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<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,
Expand Down

0 comments on commit 1fe394f

Please sign in to comment.