diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java index b797968a169..4819b862bf5 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelector.java @@ -20,6 +20,7 @@ import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -34,6 +35,7 @@ import org.springframework.util.ClassUtils; * {@link ImportAutoConfiguration}. * * @author Phillip Webb + * @author Andy Wilkinson */ class ImportAutoConfigurationImportSelector extends EnableAutoConfigurationImportSelector { @@ -66,29 +68,30 @@ class ImportAutoConfigurationImportSelector private List getCandidateConfigurations(Class source) { Set candidates = new LinkedHashSet(); - collectCandidateConfigurations(source, candidates); + collectCandidateConfigurations(source, candidates, new HashSet>()); return new ArrayList(candidates); } - private void collectCandidateConfigurations(Class source, Set candidates) { - if (source != null) { + private void collectCandidateConfigurations(Class source, Set candidates, + Set> seen) { + if (source != null && seen.add(source)) { for (Annotation annotation : source.getDeclaredAnnotations()) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { - collectCandidateConfigurations(annotation, candidates); + collectCandidateConfigurations(annotation, candidates, seen); } } - collectCandidateConfigurations(source.getSuperclass(), candidates); + collectCandidateConfigurations(source.getSuperclass(), candidates, seen); } } private void collectCandidateConfigurations(Annotation annotation, - Set candidates) { + Set candidates, Set> seen) { if (ANNOTATION_NAMES.contains(annotation.annotationType().getName())) { String[] value = (String[]) AnnotationUtils .getAnnotationAttributes(annotation, true).get("value"); candidates.addAll(Arrays.asList(value)); } - collectCandidateConfigurations(annotation.annotationType(), candidates); + collectCandidateConfigurations(annotation.annotationType(), candidates, seen); } @Override diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java index 3fd9aa5c163..e4fdb3d3a90 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/ImportAutoConfigurationImportSelectorTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure; +import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -41,6 +42,7 @@ import static org.mockito.Mockito.verifyZeroInteractions; * Tests for {@link ImportAutoConfigurationImportSelector}. * * @author Phillip Webb + * @author Andy Wilkinson */ @RunWith(MockitoJUnitRunner.class) public class ImportAutoConfigurationImportSelectorTests { @@ -87,6 +89,15 @@ public class ImportAutoConfigurationImportSelectorTests { ThymeleafAutoConfiguration.class.getName()); } + @Test + public void selfAnnotatingAnnotationDoesNotCauseStackOverflow() throws IOException { + AnnotationMetadata annotationMetadata = new SimpleMetadataReaderFactory() + .getMetadataReader(ImportWithSelfAnnotatingAnnotation.class.getName()) + .getAnnotationMetadata(); + String[] imports = this.importSelector.selectImports(annotationMetadata); + assertThat(imports).containsOnly(ThymeleafAutoConfiguration.class.getName()); + } + @ImportAutoConfiguration(FreeMarkerAutoConfiguration.class) static class ImportFreemarker { @@ -98,6 +109,11 @@ public class ImportAutoConfigurationImportSelectorTests { } + @SelfAnnotating + static class ImportWithSelfAnnotatingAnnotation { + + } + @Retention(RetentionPolicy.RUNTIME) @ImportAutoConfiguration(FreeMarkerAutoConfiguration.class) static @interface ImportOne { @@ -110,4 +126,11 @@ public class ImportAutoConfigurationImportSelectorTests { } + @Retention(RetentionPolicy.RUNTIME) + @ImportAutoConfiguration(ThymeleafAutoConfiguration.class) + @SelfAnnotating + static @interface SelfAnnotating { + + } + } diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java index 9101523583d..fa8b553085b 100644 --- a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java @@ -20,9 +20,11 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -58,13 +60,13 @@ public class AnnotationsPropertySource extends EnumerablePropertySource private Map getProperties(Class source) { Map properties = new LinkedHashMap(); - collectProperties(source, source, properties); + collectProperties(source, source, properties, new HashSet>()); return Collections.unmodifiableMap(properties); } private void collectProperties(Class root, Class source, - Map properties) { - if (source != null) { + Map properties, Set> seen) { + if (source != null && seen.add(source)) { for (Annotation annotation : getMergedAnnotations(root, source)) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { PropertyMapping typeMapping = annotation.annotationType() @@ -73,10 +75,11 @@ public class AnnotationsPropertySource extends EnumerablePropertySource .getDeclaredMethods()) { collectProperties(annotation, attribute, typeMapping, properties); } - collectProperties(root, annotation.annotationType(), properties); + collectProperties(root, annotation.annotationType(), properties, + seen); } } - collectProperties(root, source.getSuperclass(), properties); + collectProperties(root, source.getSuperclass(), properties, seen); } } diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java index 1be861be054..6335b501917 100644 --- a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java @@ -164,6 +164,11 @@ public class AnnotationsPropertySourceTests { assertThat(source.getProperty("aliasing.value")).isEqualTo("baz"); } + @Test + public void selfAnnotatingAnnotationDoesNotCauseStackOverflow() { + new AnnotationsPropertySource(PropertyMappedWithSelfAnnotatingAnnotation.class); + } + static class NoAnnotation { } @@ -327,7 +332,9 @@ public class AnnotationsPropertySourceTests { static @interface AttributeWithAliasAnnotation { @AliasFor(annotation = AliasedAttributeAnnotation.class, attribute = "value") - String value() default "foo"; + String value() + + default "foo"; String someOtherAttribute() default "shouldNotBeMapped"; @@ -341,4 +348,15 @@ public class AnnotationsPropertySourceTests { } + @Retention(RetentionPolicy.RUNTIME) + @SelfAnnotating + static @interface SelfAnnotating { + + } + + @SelfAnnotating + static class PropertyMappedWithSelfAnnotatingAnnotation { + + } + } diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java index 0e020683cc0..bed86e3ebd1 100644 --- a/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java @@ -49,6 +49,7 @@ import org.springframework.test.context.MergedContextConfiguration; * test classes. * * @author Phillip Webb + * @author Andy Wilkinson * @see ImportsContextCustomizerFactory */ class ImportsContextCustomizer implements ContextCustomizer { @@ -217,27 +218,31 @@ class ImportsContextCustomizer implements ContextCustomizer { ContextCustomizerKey(Class testClass) { Set annotations = new HashSet(); - collectClassAnnotations(testClass, annotations); + Set> seen = new HashSet>(); + collectClassAnnotations(testClass, annotations, seen); this.annotations = Collections.unmodifiableSet(annotations); } private void collectClassAnnotations(Class classType, - Set annotations) { - collectElementAnnotations(classType, annotations); - for (Class interfaceType : classType.getInterfaces()) { - collectClassAnnotations(interfaceType, annotations); - } - if (classType.getSuperclass() != null) { - collectClassAnnotations(classType.getSuperclass(), annotations); + 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); + } } } private void collectElementAnnotations(AnnotatedElement element, - Set annotations) { + Set annotations, Set> seen) { for (Annotation annotation : element.getDeclaredAnnotations()) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { annotations.add(annotation); - collectClassAnnotations(annotation.annotationType(), annotations); + collectClassAnnotations(annotation.annotationType(), annotations, + seen); } } } diff --git a/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerFactoryTests.java b/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerFactoryTests.java index 6c2f273413f..4b00a45742a 100644 --- a/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerFactoryTests.java +++ b/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerFactoryTests.java @@ -37,6 +37,7 @@ import static org.mockito.Mockito.mock; * Tests for {@link ImportsContextCustomizerFactory} and {@link ImportsContextCustomizer}. * * @author Phillip Webb + * @author Andy Wilkinson */ public class ImportsContextCustomizerFactoryTests { @@ -101,6 +102,12 @@ public class ImportsContextCustomizerFactoryTests { assertThat(context.getBean(ImportedBean.class)).isNotNull(); } + @Test + public void selfAnnotatingAnnotationDoesNotCauseStackOverflow() { + assertThat(this.factory.createContextCustomizer( + TestWithImportAndSelfAnnotatingAnnotation.class, null)).isNotNull(); + } + static class TestWithNoImport { } @@ -137,6 +144,12 @@ public class ImportsContextCustomizerFactoryTests { } + @SelfAnnotating + @Import(ImportedBean.class) + static class TestWithImportAndSelfAnnotatingAnnotation { + + } + @Retention(RetentionPolicy.RUNTIME) @Import(ImportedBean.class) @interface MetaImport { @@ -153,4 +166,10 @@ public class ImportsContextCustomizerFactoryTests { } + @Retention(RetentionPolicy.RUNTIME) + @SelfAnnotating + static @interface SelfAnnotating { + + } + }