diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java index 794aa33feb4..831e5ce20e5 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java @@ -103,8 +103,8 @@ final class AnnotationTypeMapping { private final Set claimedAliases = new HashSet<>(); - AnnotationTypeMapping(@Nullable AnnotationTypeMapping source, - Class annotationType, @Nullable Annotation annotation) { + AnnotationTypeMapping(@Nullable AnnotationTypeMapping source, Class annotationType, + @Nullable Annotation annotation, Set> visitedAnnotationTypes) { this.source = source; this.root = (source != null ? source.getRoot() : this); @@ -124,7 +124,7 @@ final class AnnotationTypeMapping { processAliases(); addConventionMappings(); addConventionAnnotationValues(); - this.synthesizable = computeSynthesizableFlag(); + this.synthesizable = computeSynthesizableFlag(visitedAnnotationTypes); } @@ -374,7 +374,10 @@ final class AnnotationTypeMapping { } @SuppressWarnings("unchecked") - private boolean computeSynthesizableFlag() { + private boolean computeSynthesizableFlag(Set> visitedAnnotationTypes) { + // Track that we have visited the current annotation type. + visitedAnnotationTypes.add(this.annotationType); + // Uses @AliasFor for local aliases? for (int index : this.aliasMappings) { if (index != -1) { @@ -403,8 +406,12 @@ final class AnnotationTypeMapping { if (type.isAnnotation() || (type.isArray() && type.getComponentType().isAnnotation())) { Class annotationType = (Class) (type.isAnnotation() ? type : type.getComponentType()); - if (annotationType != this.annotationType) { - AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(annotationType).get(0); + // Ensure we have not yet visited the current nested annotation type, in order + // to avoid infinite recursion for JVM languages other than Java that support + // recursive annotation definitions. + if (visitedAnnotationTypes.add(annotationType)) { + AnnotationTypeMapping mapping = + AnnotationTypeMappings.forAnnotationType(annotationType, visitedAnnotationTypes).get(0); if (mapping.isSynthesizable()) { return true; } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java index 7166a423208..e8e07ec311d 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -20,8 +20,10 @@ import java.lang.annotation.Annotation; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.springframework.lang.Nullable; import org.springframework.util.ConcurrentReferenceHashMap; @@ -40,6 +42,7 @@ import org.springframework.util.ConcurrentReferenceHashMap; * be searched once, regardless of how many times they are actually used. * * @author Phillip Webb + * @author Sam Brannen * @since 5.2 * @see AnnotationTypeMapping */ @@ -60,19 +63,21 @@ final class AnnotationTypeMappings { private AnnotationTypeMappings(RepeatableContainers repeatableContainers, - AnnotationFilter filter, Class annotationType) { + AnnotationFilter filter, Class annotationType, + Set> visitedAnnotationTypes) { this.repeatableContainers = repeatableContainers; this.filter = filter; this.mappings = new ArrayList<>(); - addAllMappings(annotationType); + addAllMappings(annotationType, visitedAnnotationTypes); this.mappings.forEach(AnnotationTypeMapping::afterAllMappingsSet); } - private void addAllMappings(Class annotationType) { + private void addAllMappings(Class annotationType, + Set> visitedAnnotationTypes) { Deque queue = new ArrayDeque<>(); - addIfPossible(queue, null, annotationType, null); + addIfPossible(queue, null, annotationType, null, visitedAnnotationTypes); while (!queue.isEmpty()) { AnnotationTypeMapping mapping = queue.removeFirst(); this.mappings.add(mapping); @@ -102,14 +107,15 @@ final class AnnotationTypeMappings { } private void addIfPossible(Deque queue, AnnotationTypeMapping source, Annotation ann) { - addIfPossible(queue, source, ann.annotationType(), ann); + addIfPossible(queue, source, ann.annotationType(), ann, new HashSet<>()); } private void addIfPossible(Deque queue, @Nullable AnnotationTypeMapping source, - Class annotationType, @Nullable Annotation ann) { + Class annotationType, @Nullable Annotation ann, + Set> visitedAnnotationTypes) { try { - queue.addLast(new AnnotationTypeMapping(source, annotationType, ann)); + queue.addLast(new AnnotationTypeMapping(source, annotationType, ann, visitedAnnotationTypes)); } catch (Exception ex) { AnnotationUtils.rethrowAnnotationConfigurationException(ex); @@ -166,20 +172,22 @@ final class AnnotationTypeMappings { * @return type mappings for the annotation type */ static AnnotationTypeMappings forAnnotationType(Class annotationType) { - return forAnnotationType(annotationType, AnnotationFilter.PLAIN); + return forAnnotationType(annotationType, new HashSet<>()); } /** * Create {@link AnnotationTypeMappings} for the specified annotation type. * @param annotationType the source annotation type - * @param annotationFilter the annotation filter used to limit which - * annotations are considered + * @param visitedAnnotationTypes the set of annotations that we have already + * visited; used to avoid infinite recursion for recursive annotations which + * some JVM languages support (such as Kotlin) * @return type mappings for the annotation type */ - static AnnotationTypeMappings forAnnotationType( - Class annotationType, AnnotationFilter annotationFilter) { + static AnnotationTypeMappings forAnnotationType(Class annotationType, + Set> visitedAnnotationTypes) { - return forAnnotationType(annotationType, RepeatableContainers.standardRepeatables(), annotationFilter); + return forAnnotationType(annotationType, RepeatableContainers.standardRepeatables(), + AnnotationFilter.PLAIN, visitedAnnotationTypes); } /** @@ -194,15 +202,35 @@ final class AnnotationTypeMappings { static AnnotationTypeMappings forAnnotationType(Class annotationType, RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) { + return forAnnotationType(annotationType, repeatableContainers, annotationFilter, new HashSet<>()); + } + + /** + * Create {@link AnnotationTypeMappings} for the specified annotation type. + * @param annotationType the source annotation type + * @param repeatableContainers the repeatable containers that may be used by + * the meta-annotations + * @param annotationFilter the annotation filter used to limit which + * annotations are considered + * @param visitedAnnotationTypes the set of annotations that we have already + * visited; used to avoid infinite recursion for recursive annotations which + * some JVM languages support (such as Kotlin) + * @return type mappings for the annotation type + */ + static AnnotationTypeMappings forAnnotationType(Class annotationType, + RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter, + Set> visitedAnnotationTypes) { + if (repeatableContainers == RepeatableContainers.standardRepeatables()) { return standardRepeatablesCache.computeIfAbsent(annotationFilter, - key -> new Cache(repeatableContainers, key)).get(annotationType); + key -> new Cache(repeatableContainers, key)).get(annotationType, visitedAnnotationTypes); } if (repeatableContainers == RepeatableContainers.none()) { return noRepeatablesCache.computeIfAbsent(annotationFilter, - key -> new Cache(repeatableContainers, key)).get(annotationType); + key -> new Cache(repeatableContainers, key)).get(annotationType, visitedAnnotationTypes); } - return new AnnotationTypeMappings(repeatableContainers, annotationFilter, annotationType); + return new AnnotationTypeMappings(repeatableContainers, annotationFilter, annotationType, + visitedAnnotationTypes); } static void clearCache() { @@ -235,14 +263,20 @@ final class AnnotationTypeMappings { /** * Get or create {@link AnnotationTypeMappings} for the specified annotation type. * @param annotationType the annotation type + * @param visitedAnnotationTypes the set of annotations that we have already + * visited; used to avoid infinite recursion for recursive annotations which + * some JVM languages support (such as Kotlin) * @return a new or existing {@link AnnotationTypeMappings} instance */ - AnnotationTypeMappings get(Class annotationType) { - return this.mappings.computeIfAbsent(annotationType, this::createMappings); + AnnotationTypeMappings get(Class annotationType, + Set> visitedAnnotationTypes) { + return this.mappings.computeIfAbsent(annotationType, key -> createMappings(key, visitedAnnotationTypes)); } - AnnotationTypeMappings createMappings(Class annotationType) { - return new AnnotationTypeMappings(this.repeatableContainers, this.filter, annotationType); + private AnnotationTypeMappings createMappings(Class annotationType, + Set> visitedAnnotationTypes) { + return new AnnotationTypeMappings(this.repeatableContainers, this.filter, annotationType, + visitedAnnotationTypes); } } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java index 1a440efbf63..16c5425116e 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -80,7 +80,7 @@ class AnnotationTypeMappingsTests { @Test void forAnnotationTypeWhenRepeatableMetaAnnotationIsFiltered() { AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(WithRepeatedMetaAnnotations.class, - Repeating.class.getName()::equals); + RepeatableContainers.standardRepeatables(), Repeating.class.getName()::equals); assertThat(getAll(mappings)).flatExtracting(AnnotationTypeMapping::getAnnotationType) .containsExactly(WithRepeatedMetaAnnotations.class); } diff --git a/spring-core/src/test/kotlin/org/springframework/core/annotation/FilterWithAlias.kt b/spring-core/src/test/kotlin/org/springframework/core/annotation/FilterWithAlias.kt new file mode 100644 index 00000000000..9d62f3fe138 --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/annotation/FilterWithAlias.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2023 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.annotation + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class FilterWithAlias( + + @get:AliasFor("name") + val value: String = "", + + @get:AliasFor("value") + val name: String = "", + + val and: FiltersWithoutAlias = FiltersWithoutAlias() + +) diff --git a/spring-core/src/test/kotlin/org/springframework/core/annotation/FiltersWithoutAlias.kt b/spring-core/src/test/kotlin/org/springframework/core/annotation/FiltersWithoutAlias.kt new file mode 100644 index 00000000000..2ce5c9295ac --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/annotation/FiltersWithoutAlias.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2023 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.annotation + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class FiltersWithoutAlias( + + vararg val value: FilterWithAlias + +) diff --git a/spring-core/src/test/kotlin/org/springframework/core/annotation/KotlinMergedAnnotationsTests.kt b/spring-core/src/test/kotlin/org/springframework/core/annotation/KotlinMergedAnnotationsTests.kt index 2b3cc397c67..ff65a292a0d 100644 --- a/spring-core/src/test/kotlin/org/springframework/core/annotation/KotlinMergedAnnotationsTests.kt +++ b/spring-core/src/test/kotlin/org/springframework/core/annotation/KotlinMergedAnnotationsTests.kt @@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test * * @author Sam Brannen * @author Juergen Hoeller + * @author Lorenz Simon * @since 5.3.16 */ class KotlinMergedAnnotationsTests { @@ -74,6 +75,35 @@ class KotlinMergedAnnotationsTests { assertThat(synthesizedFriends).hasSize(2) } + @Test // gh-28012 + fun recursiveNestedAnnotationWithAlias() { + val method = javaClass.getMethod("filterWithAliasMethod") + + // MergedAnnotations + val mergedAnnotations = MergedAnnotations.from(method) + assertThat(mergedAnnotations.isPresent(FilterWithAlias::class.java)).isTrue(); + + // MergedAnnotation + val mergedAnnotation = MergedAnnotation.from(method.getAnnotation(FilterWithAlias::class.java)) + assertThat(mergedAnnotation).isNotNull(); + + // Synthesized Annotations + val fooFilter = mergedAnnotation.synthesize() + assertThat(fooFilter.value).isEqualTo("foo") + assertThat(fooFilter.name).isEqualTo("foo") + val filters = fooFilter.and + assertThat(filters.value).hasSize(2) + + val barFilter = filters.value[0] + assertThat(barFilter.value).isEqualTo("bar") + assertThat(barFilter.name).isEqualTo("bar") + assertThat(barFilter.and.value).isEmpty() + + val bazFilter = filters.value[1] + assertThat(bazFilter.value).isEqualTo("baz") + assertThat(bazFilter.name).isEqualTo("baz") + assertThat(bazFilter.and.value).isEmpty() + } @PersonWithAlias("jane", friends = [PersonWithAlias("john"), PersonWithAlias("sally")]) fun personWithAliasMethod() { @@ -83,4 +113,8 @@ class KotlinMergedAnnotationsTests { fun personWithoutAliasMethod() { } + @FilterWithAlias("foo", and = FiltersWithoutAlias(FilterWithAlias("bar"), FilterWithAlias("baz"))) + fun filterWithAliasMethod() { + } + }