From baa2ef2481bdc04a801df529eb653d4c0db2923a Mon Sep 17 00:00:00 2001 From: GT Date: Sat, 15 Aug 2026 21:20:25 +0200 Subject: [PATCH 1/2] Avoid allocations for cached annotation mappings Defer creation of the visited annotation types set until a cache miss occurs. This avoids allocating a HashSet for every cached annotation mapping lookup while preserving recursive annotation handling during mapping creation. Signed-off-by: GT --- .../annotation/AnnotationTypeMappings.java | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) 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 7f1d396badf7..5692da3dfe91 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 @@ -46,6 +46,7 @@ * * @author Phillip Webb * @author Sam Brannen + * @author Greg Taube * @since 5.2 * @see AnnotationTypeMapping */ @@ -178,7 +179,7 @@ AnnotationTypeMapping get(int index) { * @return type mappings for the annotation type */ static AnnotationTypeMappings forAnnotationType(Class annotationType) { - return forAnnotationType(annotationType, new HashSet<>()); + return forAnnotationType(annotationType, RepeatableContainers.standardRepeatables(), AnnotationFilter.PLAIN); } /** @@ -208,7 +209,15 @@ static AnnotationTypeMappings forAnnotationType(Class anno static AnnotationTypeMappings forAnnotationType(Class annotationType, RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) { - return forAnnotationType(annotationType, repeatableContainers, annotationFilter, new HashSet<>()); + if (repeatableContainers == RepeatableContainers.standardRepeatables()) { + return standardRepeatablesCache.computeIfAbsent(annotationFilter, + key -> new Cache(repeatableContainers, key)).get(annotationType); + } + if (repeatableContainers == RepeatableContainers.none()) { + return noRepeatablesCache.computeIfAbsent(annotationFilter, + key -> new Cache(repeatableContainers, key)).get(annotationType); + } + return new AnnotationTypeMappings(repeatableContainers, annotationFilter, annotationType, new HashSet<>()); } /** @@ -277,6 +286,17 @@ private static class Cache { AnnotationTypeMappings get(Class annotationType, Set> visitedAnnotationTypes) { + return getOrCreate(annotationType, visitedAnnotationTypes); + } + + AnnotationTypeMappings get(Class annotationType) { + AnnotationTypeMappings result = this.mappings.get(annotationType); + return (result != null ? result : getOrCreate(annotationType, new HashSet<>())); + } + + private AnnotationTypeMappings getOrCreate(Class annotationType, + Set> visitedAnnotationTypes) { + AnnotationTypeMappings result = this.mappings.get(annotationType); if (result != null) { return result; From f0a932b0a6e7bdcf2c1447fd25d3e064f2dbaf96 Mon Sep 17 00:00:00 2001 From: GT Date: Sun, 16 Aug 2026 15:07:10 +0200 Subject: [PATCH 2/2] Polish AnnotationTypeMappings cache-hit path Extract a shared getCache helper to avoid duplicating cache selection logic, and add a test that subsequent lookups return the same cached instance. Signed-off-by: GT --- .../annotation/AnnotationTypeMappings.java | 28 +++++++++++-------- .../AnnotationTypeMappingsTests.java | 7 +++++ 2 files changed, 24 insertions(+), 11 deletions(-) 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 5692da3dfe91..305ffe9847f2 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 @@ -209,13 +209,9 @@ static AnnotationTypeMappings forAnnotationType(Class anno static AnnotationTypeMappings forAnnotationType(Class annotationType, RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) { - if (repeatableContainers == RepeatableContainers.standardRepeatables()) { - return standardRepeatablesCache.computeIfAbsent(annotationFilter, - key -> new Cache(repeatableContainers, key)).get(annotationType); - } - if (repeatableContainers == RepeatableContainers.none()) { - return noRepeatablesCache.computeIfAbsent(annotationFilter, - key -> new Cache(repeatableContainers, key)).get(annotationType); + Cache cache = getCache(repeatableContainers, annotationFilter); + if (cache != null) { + return cache.get(annotationType); } return new AnnotationTypeMappings(repeatableContainers, annotationFilter, annotationType, new HashSet<>()); } @@ -236,16 +232,26 @@ static AnnotationTypeMappings forAnnotationType(Class anno RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter, Set> visitedAnnotationTypes) { + Cache cache = getCache(repeatableContainers, annotationFilter); + if (cache != null) { + return cache.get(annotationType, visitedAnnotationTypes); + } + return new AnnotationTypeMappings(repeatableContainers, annotationFilter, annotationType, + visitedAnnotationTypes); + } + + private static @Nullable Cache getCache( + RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) { + if (repeatableContainers == RepeatableContainers.standardRepeatables()) { return standardRepeatablesCache.computeIfAbsent(annotationFilter, - key -> new Cache(repeatableContainers, key)).get(annotationType, visitedAnnotationTypes); + key -> new Cache(repeatableContainers, key)); } if (repeatableContainers == RepeatableContainers.none()) { return noRepeatablesCache.computeIfAbsent(annotationFilter, - key -> new Cache(repeatableContainers, key)).get(annotationType, visitedAnnotationTypes); + key -> new Cache(repeatableContainers, key)); } - return new AnnotationTypeMappings(repeatableContainers, annotationFilter, annotationType, - visitedAnnotationTypes); + return null; } static void clearCache() { 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 78a1e3f62fd8..7cd679bbb3ae 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 @@ -47,6 +47,13 @@ */ class AnnotationTypeMappingsTests { + @Test + void forAnnotationTypeWhenCalledTwiceReturnsCachedInstance() { + AnnotationTypeMappings first = AnnotationTypeMappings.forAnnotationType(SimpleAnnotation.class); + AnnotationTypeMappings second = AnnotationTypeMappings.forAnnotationType(SimpleAnnotation.class); + assertThat(second).isSameAs(first); + } + @Test void forAnnotationTypeWhenNoMetaAnnotationsReturnsMappings() { AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(SimpleAnnotation.class);