Avoid unnecessary allocations for cached annotation mappings

This commit defers creation of the visited annotation types set until a
cache miss occurs, which avoids allocating a HashSet for every cached
annotation mapping lookup while preserving recursive annotation
handling during mapping creation.

Closes gh-37141

Signed-off-by: GT <gregjotau@gmail.com>
This commit is contained in:
greg taube
2026-08-20 16:27:18 +02:00
committed by GitHub
parent af466ccf63
commit 59a784cb7e
2 changed files with 42 additions and 9 deletions
@@ -46,6 +46,7 @@ import org.springframework.util.ConcurrentReferenceHashMap;
*
* @author Phillip Webb
* @author Sam Brannen
* @author Greg Taube
* @since 5.2
* @see AnnotationTypeMapping
*/
@@ -178,7 +179,7 @@ final class AnnotationTypeMappings {
* @return type mappings for the annotation type
*/
static AnnotationTypeMappings forAnnotationType(Class<? extends Annotation> annotationType) {
return forAnnotationType(annotationType, new HashSet<>());
return forAnnotationType(annotationType, RepeatableContainers.standardRepeatables(), AnnotationFilter.PLAIN);
}
/**
@@ -208,7 +209,11 @@ final class AnnotationTypeMappings {
static AnnotationTypeMappings forAnnotationType(Class<? extends Annotation> annotationType,
RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter) {
return forAnnotationType(annotationType, repeatableContainers, annotationFilter, new HashSet<>());
Cache cache = getCache(repeatableContainers, annotationFilter);
if (cache != null) {
return cache.get(annotationType);
}
return new AnnotationTypeMappings(repeatableContainers, annotationFilter, annotationType, new HashSet<>());
}
/**
@@ -227,18 +232,28 @@ final class AnnotationTypeMappings {
RepeatableContainers repeatableContainers, AnnotationFilter annotationFilter,
Set<Class<? extends Annotation>> visitedAnnotationTypes) {
if (repeatableContainers == RepeatableContainers.standardRepeatables()) {
return standardRepeatablesCache.computeIfAbsent(annotationFilter,
key -> new Cache(repeatableContainers, key)).get(annotationType, visitedAnnotationTypes);
}
if (repeatableContainers == RepeatableContainers.none()) {
return noRepeatablesCache.computeIfAbsent(annotationFilter,
key -> new Cache(repeatableContainers, key)).get(annotationType, 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));
}
if (repeatableContainers == RepeatableContainers.none()) {
return noRepeatablesCache.computeIfAbsent(annotationFilter,
key -> new Cache(repeatableContainers, key));
}
return null;
}
static void clearCache() {
standardRepeatablesCache.clear();
noRepeatablesCache.clear();
@@ -277,6 +292,17 @@ final class AnnotationTypeMappings {
AnnotationTypeMappings get(Class<? extends Annotation> annotationType,
Set<Class<? extends Annotation>> visitedAnnotationTypes) {
return getOrCreate(annotationType, visitedAnnotationTypes);
}
AnnotationTypeMappings get(Class<? extends Annotation> annotationType) {
AnnotationTypeMappings result = this.mappings.get(annotationType);
return (result != null ? result : getOrCreate(annotationType, new HashSet<>()));
}
private AnnotationTypeMappings getOrCreate(Class<? extends Annotation> annotationType,
Set<Class<? extends Annotation>> visitedAnnotationTypes) {
AnnotationTypeMappings result = this.mappings.get(annotationType);
if (result != null) {
return result;
@@ -47,6 +47,13 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
*/
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);