Protect against stack overflow when searching meta annotations

It is legal for an annotation to be annotated with itself. Previously,
when searching for meta annotations this could lead to a stack overflow.
This was likely to occur when using Kotlin as, like Java, its Target
annotation is annotated with itself. A stack overflow doesn’t occur
with Java’s Target annotation due to some short-circuiting logic for
annotations in java.lang.

This commit updates the logic for finding meta-annotations to
short-circuit when an annotation that has already been seen is
encountered.

Closes gh-5902
This commit is contained in:
Andy Wilkinson 2016-05-10 09:39:38 +01:00
parent 699d083cec
commit 0a765e36f1
6 changed files with 94 additions and 23 deletions

View File

@ -20,6 +20,7 @@ import java.lang.annotation.Annotation;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -34,6 +35,7 @@ import org.springframework.util.ClassUtils;
* {@link ImportAutoConfiguration}. * {@link ImportAutoConfiguration}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson
*/ */
class ImportAutoConfigurationImportSelector class ImportAutoConfigurationImportSelector
extends EnableAutoConfigurationImportSelector { extends EnableAutoConfigurationImportSelector {
@ -66,29 +68,30 @@ class ImportAutoConfigurationImportSelector
private List<String> getCandidateConfigurations(Class<?> source) { private List<String> getCandidateConfigurations(Class<?> source) {
Set<String> candidates = new LinkedHashSet<String>(); Set<String> candidates = new LinkedHashSet<String>();
collectCandidateConfigurations(source, candidates); collectCandidateConfigurations(source, candidates, new HashSet<Class<?>>());
return new ArrayList<String>(candidates); return new ArrayList<String>(candidates);
} }
private void collectCandidateConfigurations(Class<?> source, Set<String> candidates) { private void collectCandidateConfigurations(Class<?> source, Set<String> candidates,
if (source != null) { Set<Class<?>> seen) {
if (source != null && seen.add(source)) {
for (Annotation annotation : source.getDeclaredAnnotations()) { for (Annotation annotation : source.getDeclaredAnnotations()) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { 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, private void collectCandidateConfigurations(Annotation annotation,
Set<String> candidates) { Set<String> candidates, Set<Class<?>> seen) {
if (ANNOTATION_NAMES.contains(annotation.annotationType().getName())) { if (ANNOTATION_NAMES.contains(annotation.annotationType().getName())) {
String[] value = (String[]) AnnotationUtils String[] value = (String[]) AnnotationUtils
.getAnnotationAttributes(annotation, true).get("value"); .getAnnotationAttributes(annotation, true).get("value");
candidates.addAll(Arrays.asList(value)); candidates.addAll(Arrays.asList(value));
} }
collectCandidateConfigurations(annotation.annotationType(), candidates); collectCandidateConfigurations(annotation.annotationType(), candidates, seen);
} }
@Override @Override

View File

@ -16,6 +16,7 @@
package org.springframework.boot.autoconfigure; package org.springframework.boot.autoconfigure;
import java.io.IOException;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@ -41,6 +42,7 @@ import static org.mockito.Mockito.verifyZeroInteractions;
* Tests for {@link ImportAutoConfigurationImportSelector}. * Tests for {@link ImportAutoConfigurationImportSelector}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson
*/ */
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class ImportAutoConfigurationImportSelectorTests { public class ImportAutoConfigurationImportSelectorTests {
@ -87,6 +89,15 @@ public class ImportAutoConfigurationImportSelectorTests {
ThymeleafAutoConfiguration.class.getName()); 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) @ImportAutoConfiguration(FreeMarkerAutoConfiguration.class)
static class ImportFreemarker { static class ImportFreemarker {
@ -98,6 +109,11 @@ public class ImportAutoConfigurationImportSelectorTests {
} }
@SelfAnnotating
static class ImportWithSelfAnnotatingAnnotation {
}
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@ImportAutoConfiguration(FreeMarkerAutoConfiguration.class) @ImportAutoConfiguration(FreeMarkerAutoConfiguration.class)
static @interface ImportOne { static @interface ImportOne {
@ -110,4 +126,11 @@ public class ImportAutoConfigurationImportSelectorTests {
} }
@Retention(RetentionPolicy.RUNTIME)
@ImportAutoConfiguration(ThymeleafAutoConfiguration.class)
@SelfAnnotating
static @interface SelfAnnotating {
}
} }

View File

@ -20,9 +20,11 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -58,13 +60,13 @@ public class AnnotationsPropertySource extends EnumerablePropertySource<Class<?>
private Map<String, Object> getProperties(Class<?> source) { private Map<String, Object> getProperties(Class<?> source) {
Map<String, Object> properties = new LinkedHashMap<String, Object>(); Map<String, Object> properties = new LinkedHashMap<String, Object>();
collectProperties(source, source, properties); collectProperties(source, source, properties, new HashSet<Class<?>>());
return Collections.unmodifiableMap(properties); return Collections.unmodifiableMap(properties);
} }
private void collectProperties(Class<?> root, Class<?> source, private void collectProperties(Class<?> root, Class<?> source,
Map<String, Object> properties) { Map<String, Object> properties, Set<Class<?>> seen) {
if (source != null) { if (source != null && seen.add(source)) {
for (Annotation annotation : getMergedAnnotations(root, source)) { for (Annotation annotation : getMergedAnnotations(root, source)) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
PropertyMapping typeMapping = annotation.annotationType() PropertyMapping typeMapping = annotation.annotationType()
@ -73,10 +75,11 @@ public class AnnotationsPropertySource extends EnumerablePropertySource<Class<?>
.getDeclaredMethods()) { .getDeclaredMethods()) {
collectProperties(annotation, attribute, typeMapping, properties); 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);
} }
} }

View File

@ -164,6 +164,11 @@ public class AnnotationsPropertySourceTests {
assertThat(source.getProperty("aliasing.value")).isEqualTo("baz"); assertThat(source.getProperty("aliasing.value")).isEqualTo("baz");
} }
@Test
public void selfAnnotatingAnnotationDoesNotCauseStackOverflow() {
new AnnotationsPropertySource(PropertyMappedWithSelfAnnotatingAnnotation.class);
}
static class NoAnnotation { static class NoAnnotation {
} }
@ -327,7 +332,9 @@ public class AnnotationsPropertySourceTests {
static @interface AttributeWithAliasAnnotation { static @interface AttributeWithAliasAnnotation {
@AliasFor(annotation = AliasedAttributeAnnotation.class, attribute = "value") @AliasFor(annotation = AliasedAttributeAnnotation.class, attribute = "value")
String value() default "foo"; String value()
default "foo";
String someOtherAttribute() default "shouldNotBeMapped"; String someOtherAttribute() default "shouldNotBeMapped";
@ -341,4 +348,15 @@ public class AnnotationsPropertySourceTests {
} }
@Retention(RetentionPolicy.RUNTIME)
@SelfAnnotating
static @interface SelfAnnotating {
}
@SelfAnnotating
static class PropertyMappedWithSelfAnnotatingAnnotation {
}
} }

View File

@ -49,6 +49,7 @@ import org.springframework.test.context.MergedContextConfiguration;
* test classes. * test classes.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson
* @see ImportsContextCustomizerFactory * @see ImportsContextCustomizerFactory
*/ */
class ImportsContextCustomizer implements ContextCustomizer { class ImportsContextCustomizer implements ContextCustomizer {
@ -217,27 +218,31 @@ class ImportsContextCustomizer implements ContextCustomizer {
ContextCustomizerKey(Class<?> testClass) { ContextCustomizerKey(Class<?> testClass) {
Set<Annotation> annotations = new HashSet<Annotation>(); Set<Annotation> annotations = new HashSet<Annotation>();
collectClassAnnotations(testClass, annotations); Set<Class<?>> seen = new HashSet<Class<?>>();
collectClassAnnotations(testClass, annotations, seen);
this.annotations = Collections.unmodifiableSet(annotations); this.annotations = Collections.unmodifiableSet(annotations);
} }
private void collectClassAnnotations(Class<?> classType, private void collectClassAnnotations(Class<?> classType,
Set<Annotation> annotations) { Set<Annotation> annotations, Set<Class<?>> seen) {
collectElementAnnotations(classType, annotations); if (seen.add(classType)) {
for (Class<?> interfaceType : classType.getInterfaces()) { collectElementAnnotations(classType, annotations, seen);
collectClassAnnotations(interfaceType, annotations); for (Class<?> interfaceType : classType.getInterfaces()) {
} collectClassAnnotations(interfaceType, annotations, seen);
if (classType.getSuperclass() != null) { }
collectClassAnnotations(classType.getSuperclass(), annotations); if (classType.getSuperclass() != null) {
collectClassAnnotations(classType.getSuperclass(), annotations, seen);
}
} }
} }
private void collectElementAnnotations(AnnotatedElement element, private void collectElementAnnotations(AnnotatedElement element,
Set<Annotation> annotations) { Set<Annotation> annotations, Set<Class<?>> seen) {
for (Annotation annotation : element.getDeclaredAnnotations()) { for (Annotation annotation : element.getDeclaredAnnotations()) {
if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
annotations.add(annotation); annotations.add(annotation);
collectClassAnnotations(annotation.annotationType(), annotations); collectClassAnnotations(annotation.annotationType(), annotations,
seen);
} }
} }
} }

View File

@ -37,6 +37,7 @@ import static org.mockito.Mockito.mock;
* Tests for {@link ImportsContextCustomizerFactory} and {@link ImportsContextCustomizer}. * Tests for {@link ImportsContextCustomizerFactory} and {@link ImportsContextCustomizer}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson
*/ */
public class ImportsContextCustomizerFactoryTests { public class ImportsContextCustomizerFactoryTests {
@ -101,6 +102,12 @@ public class ImportsContextCustomizerFactoryTests {
assertThat(context.getBean(ImportedBean.class)).isNotNull(); assertThat(context.getBean(ImportedBean.class)).isNotNull();
} }
@Test
public void selfAnnotatingAnnotationDoesNotCauseStackOverflow() {
assertThat(this.factory.createContextCustomizer(
TestWithImportAndSelfAnnotatingAnnotation.class, null)).isNotNull();
}
static class TestWithNoImport { static class TestWithNoImport {
} }
@ -137,6 +144,12 @@ public class ImportsContextCustomizerFactoryTests {
} }
@SelfAnnotating
@Import(ImportedBean.class)
static class TestWithImportAndSelfAnnotatingAnnotation {
}
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Import(ImportedBean.class) @Import(ImportedBean.class)
@interface MetaImport { @interface MetaImport {
@ -153,4 +166,10 @@ public class ImportsContextCustomizerFactoryTests {
} }
@Retention(RetentionPolicy.RUNTIME)
@SelfAnnotating
static @interface SelfAnnotating {
}
} }