From 4562189125d2894e27aaac122591aac44f2ef1bb Mon Sep 17 00:00:00 2001 From: Laurent Martelli Date: Tue, 4 Jul 2023 12:22:57 +0100 Subject: [PATCH 1/2] Switch ImportsContextCustomizer to use MergedAnnotations.search #36211 Use `MergedAnnotations.search` in `ImportsContextCustomizer` rather than needing dedicated search logic. See gh-36211 --- .../context/ImportsContextCustomizer.java | 167 ++++-------------- 1 file changed, 36 insertions(+), 131 deletions(-) diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java index fc4a8babbf..4b8a6a7531 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java @@ -16,13 +16,12 @@ package org.springframework.boot.test.context; -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -42,10 +41,10 @@ import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.context.annotation.ImportSelector; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.core.Ordered; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationFilter; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.annotation.Order; import org.springframework.core.style.ToStringCreator; import org.springframework.core.type.AnnotationMetadata; @@ -53,6 +52,8 @@ import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.util.ReflectionUtils; +import static org.springframework.core.annotation.AnnotationFilter.packages; + /** * {@link ContextCustomizer} to allow {@code @Import} annotations to be used directly on * test classes. @@ -217,82 +218,41 @@ class ImportsContextCustomizer implements ContextCustomizer { */ static class ContextCustomizerKey { - private static final Class[] NO_IMPORTS = {}; + private static final AnnotationFilter ANNOTATION_FILTERS = or(packages("java.lang.annotation"), + packages("org.spockframework", "spock"), + or(isEqualTo("kotlin.Metadata"), packages("kotlin.annotation")), packages(("org.junit"))); - private static final Set ANNOTATION_FILTERS; - - static { - Set filters = new HashSet<>(); - filters.add(new JavaLangAnnotationFilter()); - filters.add(new KotlinAnnotationFilter()); - filters.add(new SpockAnnotationFilter()); - filters.add(new JUnitAnnotationFilter()); - ANNOTATION_FILTERS = Collections.unmodifiableSet(filters); - } - - private final Set key; + private final Object key; ContextCustomizerKey(Class testClass) { - Set annotations = new HashSet<>(); - Set> seen = new HashSet<>(); - collectClassAnnotations(testClass, annotations, seen); - Set determinedImports = determineImports(annotations, testClass); - this.key = Collections.unmodifiableSet((determinedImports != null) ? determinedImports : annotations); - } - - private void collectClassAnnotations(Class classType, Set annotations, Set> seen) { - if (seen.add(classType)) { - collectElementAnnotations(classType, annotations, seen); - for (Class interfaceType : classType.getInterfaces()) { - collectClassAnnotations(interfaceType, annotations, seen); - } - if (classType.getSuperclass() != null) { - collectClassAnnotations(classType.getSuperclass(), annotations, seen); - } + MergedAnnotations mergedAnnotations = MergedAnnotations + .search(MergedAnnotations.SearchStrategy.TYPE_HIERARCHY) + .withAnnotationFilter(ANNOTATION_FILTERS) + .from(testClass); + Set determinedImports = determineImports(mergedAnnotations, testClass); + if (determinedImports != null) { + this.key = determinedImports; + } + else { + this.key = AnnotatedElementUtils.findAllMergedAnnotations(testClass, + mergedAnnotations.stream().map(MergedAnnotation::getType).collect(Collectors.toSet())); } } - private void collectElementAnnotations(AnnotatedElement element, Set annotations, - Set> seen) { - for (MergedAnnotation mergedAnnotation : MergedAnnotations.from(element, - SearchStrategy.DIRECT)) { - Annotation annotation = mergedAnnotation.synthesize(); - if (!isIgnoredAnnotation(annotation)) { - annotations.add(annotation); - collectClassAnnotations(annotation.annotationType(), annotations, seen); - } - } - } - - private boolean isIgnoredAnnotation(Annotation annotation) { - for (AnnotationFilter annotationFilter : ANNOTATION_FILTERS) { - if (annotationFilter.isIgnored(annotation)) { - return true; - } - } - return false; - } - - private Set determineImports(Set annotations, Class testClass) { - Set determinedImports = new LinkedHashSet<>(); + private Set determineImports(MergedAnnotations mergedAnnotations, Class testClass) { AnnotationMetadata testClassMetadata = AnnotationMetadata.introspect(testClass); - for (Annotation annotation : annotations) { - for (Class source : getImports(annotation)) { - Set determinedSourceImports = determineImports(source, testClassMetadata); - if (determinedSourceImports == null) { + return mergedAnnotations.stream(Import.class) + .flatMap((ma) -> Stream.of(ma.getClassArray("value"))) + .map((source) -> determineImports(source, testClassMetadata)) + .reduce(new HashSet<>(), (a, b) -> { + if (a == null || b == null) { return null; } - determinedImports.addAll(determinedSourceImports); - } - } - return determinedImports; - } - - private Class[] getImports(Annotation annotation) { - if (annotation instanceof Import importAnnotation) { - return importAnnotation.value(); - } - return NO_IMPORTS; + else { + a.add(b); + return a; + } + }); } private Set determineImports(Class source, AnnotationMetadata metadata) { @@ -340,67 +300,12 @@ class ImportsContextCustomizer implements ContextCustomizer { } - /** - * Filter used to limit considered annotations. - */ - private interface AnnotationFilter { - - boolean isIgnored(Annotation annotation); - + static AnnotationFilter or(AnnotationFilter... filters) { + return typeName -> Stream.of(filters).anyMatch(filter -> filter.matches(typeName)); } - /** - * {@link AnnotationFilter} for {@literal java.lang} annotations. - */ - private static final class JavaLangAnnotationFilter implements AnnotationFilter { - - @Override - public boolean isIgnored(Annotation annotation) { - return AnnotationUtils.isInJavaLangAnnotationPackage(annotation); - } - - } - - /** - * {@link AnnotationFilter} for Kotlin annotations. - */ - private static final class KotlinAnnotationFilter implements AnnotationFilter { - - @Override - public boolean isIgnored(Annotation annotation) { - return "kotlin.Metadata".equals(annotation.annotationType().getName()) - || isInKotlinAnnotationPackage(annotation); - } - - private boolean isInKotlinAnnotationPackage(Annotation annotation) { - return annotation.annotationType().getName().startsWith("kotlin.annotation."); - } - - } - - /** - * {@link AnnotationFilter} for Spock annotations. - */ - private static final class SpockAnnotationFilter implements AnnotationFilter { - - @Override - public boolean isIgnored(Annotation annotation) { - return annotation.annotationType().getName().startsWith("org.spockframework.") - || annotation.annotationType().getName().startsWith("spock."); - } - - } - - /** - * {@link AnnotationFilter} for JUnit annotations. - */ - private static final class JUnitAnnotationFilter implements AnnotationFilter { - - @Override - public boolean isIgnored(Annotation annotation) { - return annotation.annotationType().getName().startsWith("org.junit."); - } - + static AnnotationFilter isEqualTo(String expectedTypeName) { + return typeName -> typeName.equals(expectedTypeName); } } From 7c942679ad85f254acaa0318cf07753c65a21716 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 4 Jul 2023 12:49:52 +0100 Subject: [PATCH 2/2] Polish 'Switch ImportsContextCustomizer to use MergedAnnotations.search' See gh-36211 --- .../context/ImportsContextCustomizer.java | 77 +++++++++---------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java index 4b8a6a7531..ca5786240b 100644 --- a/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java +++ b/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java @@ -18,10 +18,9 @@ package org.springframework.boot.test.context; import java.lang.reflect.Constructor; import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -41,7 +40,6 @@ import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.context.annotation.ImportSelector; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.core.Ordered; -import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationFilter; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; @@ -52,14 +50,13 @@ import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.util.ReflectionUtils; -import static org.springframework.core.annotation.AnnotationFilter.packages; - /** * {@link ContextCustomizer} to allow {@code @Import} annotations to be used directly on * test classes. * * @author Phillip Webb * @author Andy Wilkinson + * @author Laurent Martelli * @see ImportsContextCustomizerFactory */ class ImportsContextCustomizer implements ContextCustomizer { @@ -218,41 +215,43 @@ class ImportsContextCustomizer implements ContextCustomizer { */ static class ContextCustomizerKey { - private static final AnnotationFilter ANNOTATION_FILTERS = or(packages("java.lang.annotation"), - packages("org.spockframework", "spock"), - or(isEqualTo("kotlin.Metadata"), packages("kotlin.annotation")), packages(("org.junit"))); - - private final Object key; + private static final Set ANNOTATION_FILTERS; + static { + Set annotationFilters = new LinkedHashSet<>(); + annotationFilters.add(AnnotationFilter.PLAIN); + annotationFilters.add("kotlin.Metadata"::equals); + annotationFilters.add(AnnotationFilter.packages("kotlin.annotation")); + annotationFilters.add(AnnotationFilter.packages("org.spockframework", "spock")); + annotationFilters.add(AnnotationFilter.packages("org.junit")); + ANNOTATION_FILTERS = Collections.unmodifiableSet(annotationFilters); + } + private final Set key; ContextCustomizerKey(Class testClass) { - MergedAnnotations mergedAnnotations = MergedAnnotations - .search(MergedAnnotations.SearchStrategy.TYPE_HIERARCHY) - .withAnnotationFilter(ANNOTATION_FILTERS) + MergedAnnotations annotations = MergedAnnotations.search(MergedAnnotations.SearchStrategy.TYPE_HIERARCHY) + .withAnnotationFilter(this::isFilteredAnnotation) .from(testClass); - Set determinedImports = determineImports(mergedAnnotations, testClass); - if (determinedImports != null) { - this.key = determinedImports; - } - else { - this.key = AnnotatedElementUtils.findAllMergedAnnotations(testClass, - mergedAnnotations.stream().map(MergedAnnotation::getType).collect(Collectors.toSet())); - } + Set determinedImports = determineImports(annotations, testClass); + this.key = (determinedImports != null) ? determinedImports : synthesize(annotations); } - private Set determineImports(MergedAnnotations mergedAnnotations, Class testClass) { - AnnotationMetadata testClassMetadata = AnnotationMetadata.introspect(testClass); - return mergedAnnotations.stream(Import.class) - .flatMap((ma) -> Stream.of(ma.getClassArray("value"))) - .map((source) -> determineImports(source, testClassMetadata)) - .reduce(new HashSet<>(), (a, b) -> { - if (a == null || b == null) { + private boolean isFilteredAnnotation(String typeName) { + return ANNOTATION_FILTERS.stream().anyMatch((filter) -> filter.matches(typeName)); + } + + private Set determineImports(MergedAnnotations annotations, Class testClass) { + Set determinedImports = new LinkedHashSet<>(); + AnnotationMetadata metadata = AnnotationMetadata.introspect(testClass); + for (MergedAnnotation annotation : annotations.stream(Import.class).toList()) { + for (Class source : annotation.getClassArray(MergedAnnotation.VALUE)) { + Set determinedSourceImports = determineImports(source, metadata); + if (determinedSourceImports == null) { return null; } - else { - a.add(b); - return a; - } - }); + determinedImports.addAll(determinedSourceImports); + } + } + return determinedImports; } private Set determineImports(Class source, AnnotationMetadata metadata) { @@ -270,6 +269,10 @@ class ImportsContextCustomizer implements ContextCustomizer { return Collections.singleton(source.getName()); } + private Set synthesize(MergedAnnotations annotations) { + return annotations.stream().map(MergedAnnotation::synthesize).collect(Collectors.toSet()); + } + @SuppressWarnings("unchecked") private T instantiate(Class source) { try { @@ -300,12 +303,4 @@ class ImportsContextCustomizer implements ContextCustomizer { } - static AnnotationFilter or(AnnotationFilter... filters) { - return typeName -> Stream.of(filters).anyMatch(filter -> filter.matches(typeName)); - } - - static AnnotationFilter isEqualTo(String expectedTypeName) { - return typeName -> typeName.equals(expectedTypeName); - } - }