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:
parent
699d083cec
commit
0a765e36f1
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue