Skip to content

Commit 3ec612a

Browse files
committed
Support recursive annotations in merged annotations
Although Java does not allow the definition of recursive annotations, Kotlin does, and prior to this commit an attempt to synthesize a merged annotation using the MergedAnnotation API resulted in a StackOverflowError if there was a recursive cycle in the annotation definitions. This commit addresses this issue by tracking which annotations have already been visited and short circuits the recursive algorithm if a cycle is detected. Closes gh-28012
1 parent 4eaee1e commit 3ec612a

File tree

7 files changed

+279
-31
lines changed

7 files changed

+279
-31
lines changed

spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -82,8 +82,8 @@ final class AnnotationTypeMapping {
8282
private final Set<Method> claimedAliases = new HashSet<>();
8383

8484

85-
AnnotationTypeMapping(@Nullable AnnotationTypeMapping source,
86-
Class<? extends Annotation> annotationType, @Nullable Annotation annotation) {
85+
AnnotationTypeMapping(@Nullable AnnotationTypeMapping source, Class<? extends Annotation> annotationType,
86+
@Nullable Annotation annotation, Set<Class<? extends Annotation>> visitedAnnotationTypes) {
8787

8888
this.source = source;
8989
this.root = (source != null ? source.getRoot() : this);
@@ -103,7 +103,7 @@ final class AnnotationTypeMapping {
103103
processAliases();
104104
addConventionMappings();
105105
addConventionAnnotationValues();
106-
this.synthesizable = computeSynthesizableFlag();
106+
this.synthesizable = computeSynthesizableFlag(visitedAnnotationTypes);
107107
}
108108

109109

@@ -311,7 +311,10 @@ private boolean isBetterConventionAnnotationValue(int index, boolean isValueAttr
311311
}
312312

313313
@SuppressWarnings("unchecked")
314-
private boolean computeSynthesizableFlag() {
314+
private boolean computeSynthesizableFlag(Set<Class<? extends Annotation>> visitedAnnotationTypes) {
315+
// Track that we have visited the current annotation type.
316+
visitedAnnotationTypes.add(this.annotationType);
317+
315318
// Uses @AliasFor for local aliases?
316319
for (int index : this.aliasMappings) {
317320
if (index != -1) {
@@ -340,9 +343,15 @@ private boolean computeSynthesizableFlag() {
340343
if (type.isAnnotation() || (type.isArray() && type.getComponentType().isAnnotation())) {
341344
Class<? extends Annotation> annotationType =
342345
(Class<? extends Annotation>) (type.isAnnotation() ? type : type.getComponentType());
343-
AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(annotationType).get(0);
344-
if (mapping.isSynthesizable()) {
345-
return true;
346+
// Ensure we have not yet visited the current nested annotation type, in order
347+
// to avoid infinite recursion for JVM languages other than Java that support
348+
// recursive annotation definitions.
349+
if (visitedAnnotationTypes.add(annotationType)) {
350+
AnnotationTypeMapping mapping =
351+
AnnotationTypeMappings.forAnnotationType(annotationType, visitedAnnotationTypes).get(0);
352+
if (mapping.isSynthesizable()) {
353+
return true;
354+
}
346355
}
347356
}
348357
}

spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,8 +20,10 @@
2020
import java.util.ArrayDeque;
2121
import java.util.ArrayList;
2222
import java.util.Deque;
23+
import java.util.HashSet;
2324
import java.util.List;
2425
import java.util.Map;
26+
import java.util.Set;
2527

2628
import org.springframework.lang.Nullable;
2729
import org.springframework.util.ConcurrentReferenceHashMap;
@@ -40,6 +42,7 @@
4042
* be searched once, regardless of how many times they are actually used.
4143
*
4244
* @author Phillip Webb
45+
* @author Sam Brannen
4346
* @since 5.2
4447
* @see AnnotationTypeMapping
4548
*/
@@ -60,19 +63,22 @@ final class AnnotationTypeMappings {
6063

6164

6265
private AnnotationTypeMappings(RepeatableContainers repeatableContainers,
63-
AnnotationFilter filter, Class<? extends Annotation> annotationType) {
66+
AnnotationFilter filter, Class<? extends Annotation> annotationType,
67+
Set<Class<? extends Annotation>> visitedAnnotationTypes) {
6468

6569
this.repeatableContainers = repeatableContainers;
6670
this.filter = filter;
6771
this.mappings = new ArrayList<>();
68-
addAllMappings(annotationType);
72+
addAllMappings(annotationType, visitedAnnotationTypes);
6973
this.mappings.forEach(AnnotationTypeMapping::afterAllMappingsSet);
7074
}
7175

7276

73-
private void addAllMappings(Class<? extends Annotation> annotationType) {
77+
private void addAllMappings(Class<? extends Annotation> annotationType,
78+
Set<Class<? extends Annotation>> visitedAnnotationTypes) {
79+
7480
Deque<AnnotationTypeMapping> queue = new ArrayDeque<>();
75-
addIfPossible(queue, null, annotationType, null);
81+
addIfPossible(queue, null, annotationType, null, visitedAnnotationTypes);
7682
while (!queue.isEmpty()) {
7783
AnnotationTypeMapping mapping = queue.removeFirst();
7884
this.mappings.add(mapping);
@@ -102,14 +108,15 @@ private void addMetaAnnotationsToQueue(Deque<AnnotationTypeMapping> queue, Annot
102108
}
103109

104110
private void addIfPossible(Deque<AnnotationTypeMapping> queue, AnnotationTypeMapping source, Annotation ann) {
105-
addIfPossible(queue, source, ann.annotationType(), ann);
111+
addIfPossible(queue, source, ann.annotationType(), ann, new HashSet<>());
106112
}
107113

108114
private void addIfPossible(Deque<AnnotationTypeMapping> queue, @Nullable AnnotationTypeMapping source,
109-
Class<? extends Annotation> annotationType, @Nullable Annotation ann) {
115+
Class<? extends Annotation> annotationType, @Nullable Annotation ann,
116+
Set<Class<? extends Annotation>> visitedAnnotationTypes) {
110117

111118
try {
112-
queue.addLast(new AnnotationTypeMapping(source, annotationType, ann));
119+
queue.addLast(new AnnotationTypeMapping(source, annotationType, ann, visitedAnnotationTypes));
113120
}
114121
catch (Exception ex) {
115122
AnnotationUtils.rethrowAnnotationConfigurationException(ex);
@@ -166,20 +173,22 @@ AnnotationTypeMapping get(int index) {
166173
* @return type mappings for the annotation type
167174
*/
168175
static AnnotationTypeMappings forAnnotationType(Class<? extends Annotation> annotationType) {
169-
return forAnnotationType(annotationType, AnnotationFilter.PLAIN);
176+
return forAnnotationType(annotationType, new HashSet<>());
170177
}
171178

172179
/**
173180
* Create {@link AnnotationTypeMappings} for the specified annotation type.
174181
* @param annotationType the source annotation type
175-
* @param annotationFilter the annotation filter used to limit which
176-
* annotations are considered
182+
* @param visitedAnnotationTypes the set of annotations that we have already
183+
* visited; used to avoid infinite recursion for recursive annotations which
184+
* some JVM languages support (such as Kotlin)
177185
* @return type mappings for the annotation type
178186
*/
179-
static AnnotationTypeMappings forAnnotationType(
180-
Class<? extends Annotation> annotationType, AnnotationFilter annotationFilter) {
187+
static AnnotationTypeMappings forAnnotationType(Class<? extends Annotation> annotationType,
188+
Set<Class<? extends Annotation>> visitedAnnotationTypes) {
181189

182-
return forAnnotationType(annotationType, RepeatableContainers.standardRepeatables(), annotationFilter);
190+
return forAnnotationType(annotationType, RepeatableContainers.standardRepeatables(),
191+
AnnotationFilter.PLAIN, visitedAnnotationTypes);
183192
}
184193

185194
/**
@@ -194,15 +203,34 @@ static AnnotationTypeMappings forAnnotationType(
194203
static AnnotationTypeMappings forAnnotationType(Class<? extends Annotation> annotationType,
195204
RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) {
196205

206+
return forAnnotationType(annotationType, repeatableContainers, annotationFilter, new HashSet<>());
207+
}
208+
209+
/**
210+
* Create {@link AnnotationTypeMappings} for the specified annotation type.
211+
* @param annotationType the source annotation type
212+
* @param repeatableContainers the repeatable containers that may be used by
213+
* the meta-annotations
214+
* @param annotationFilter the annotation filter used to limit which
215+
* annotations are considered
216+
* @param visitedAnnotationTypes the set of annotations that we have already
217+
* visited; used to avoid infinite recursion for recursive annotations which
218+
* some JVM languages support (such as Kotlin)
219+
* @return type mappings for the annotation type
220+
*/
221+
private static AnnotationTypeMappings forAnnotationType(Class<? extends Annotation> annotationType,
222+
RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter,
223+
Set<Class<? extends Annotation>> visitedAnnotationTypes) {
224+
197225
if (repeatableContainers == RepeatableContainers.standardRepeatables()) {
198226
return standardRepeatablesCache.computeIfAbsent(annotationFilter,
199-
key -> new Cache(repeatableContainers, key)).get(annotationType);
227+
key -> new Cache(repeatableContainers, key)).get(annotationType, visitedAnnotationTypes);
200228
}
201229
if (repeatableContainers == RepeatableContainers.none()) {
202230
return noRepeatablesCache.computeIfAbsent(annotationFilter,
203-
key -> new Cache(repeatableContainers, key)).get(annotationType);
231+
key -> new Cache(repeatableContainers, key)).get(annotationType, visitedAnnotationTypes);
204232
}
205-
return new AnnotationTypeMappings(repeatableContainers, annotationFilter, annotationType);
233+
return new AnnotationTypeMappings(repeatableContainers, annotationFilter, annotationType, visitedAnnotationTypes);
206234
}
207235

208236
static void clearCache() {
@@ -235,14 +263,21 @@ private static class Cache {
235263
/**
236264
* Get or create {@link AnnotationTypeMappings} for the specified annotation type.
237265
* @param annotationType the annotation type
266+
* @param visitedAnnotationTypes the set of annotations that we have already
267+
* visited; used to avoid infinite recursion for recursive annotations which
268+
* some JVM languages support (such as Kotlin)
238269
* @return a new or existing {@link AnnotationTypeMappings} instance
239270
*/
240-
AnnotationTypeMappings get(Class<? extends Annotation> annotationType) {
241-
return this.mappings.computeIfAbsent(annotationType, this::createMappings);
271+
AnnotationTypeMappings get(Class<? extends Annotation> annotationType,
272+
Set<Class<? extends Annotation>> visitedAnnotationTypes) {
273+
274+
return this.mappings.computeIfAbsent(annotationType, key -> createMappings(key, visitedAnnotationTypes));
242275
}
243276

244-
AnnotationTypeMappings createMappings(Class<? extends Annotation> annotationType) {
245-
return new AnnotationTypeMappings(this.repeatableContainers, this.filter, annotationType);
277+
private AnnotationTypeMappings createMappings(Class<? extends Annotation> annotationType,
278+
Set<Class<? extends Annotation>> visitedAnnotationTypes) {
279+
280+
return new AnnotationTypeMappings(this.repeatableContainers, this.filter, annotationType, visitedAnnotationTypes);
246281
}
247282
}
248283

spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -88,7 +88,7 @@ void forAnnotationTypeWhenHasRepeatingMetaAnnotationReturnsMapping() {
8888
@Test
8989
void forAnnotationTypeWhenRepeatableMetaAnnotationIsFiltered() {
9090
AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(WithRepeatedMetaAnnotations.class,
91-
Repeating.class.getName()::equals);
91+
RepeatableContainers.standardRepeatables(), Repeating.class.getName()::equals);
9292
assertThat(getAll(mappings)).flatExtracting(AnnotationTypeMapping::getAnnotationType)
9393
.containsExactly(WithRepeatedMetaAnnotations.class);
9494
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.core.annotation
18+
19+
/**
20+
* @author Sam Brannen
21+
* @since 5.3.16
22+
*/
23+
@Target(AnnotationTarget.FUNCTION)
24+
@Retention(AnnotationRetention.RUNTIME)
25+
public annotation class Filter(
26+
27+
@get:AliasFor("name")
28+
val value: String = "",
29+
30+
@get:AliasFor("value")
31+
val name: String = "",
32+
33+
val and: Filters = Filters()
34+
35+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.core.annotation
18+
19+
/**
20+
* @author Sam Brannen
21+
* @since 5.3.16
22+
*/
23+
@Target(AnnotationTarget.FUNCTION)
24+
@Retention(AnnotationRetention.RUNTIME)
25+
public annotation class Filters(
26+
27+
vararg val value: Filter
28+
29+
)

0 commit comments

Comments
 (0)