From 47706be6e4fbb4a486cd50b68090e709ac368bb8 Mon Sep 17 00:00:00 2001 From: ParkJuhyeong Date: Sat, 15 Nov 2025 02:23:34 +0900 Subject: [PATCH] Fix memory leak in CaffeineCacheManager when switching to static mode When setCacheNames() is called to switch from dynamic to static mode, dynamically created caches were not removed from the internal cacheMap, causing: 1. Memory leak (orphaned cache references) 2. Violation of Javadoc contract stating cache names will be 'fixed' 3. getCacheNames() returning unexpected cache names This fix ensures that dynamically created caches are cleared when switching to static mode, while preserving custom caches registered via registerCustomCache(). Signed-off-by: ParkJuhyeong --- .../cache/caffeine/CaffeineCacheManager.java | 7 ++ .../caffeine/CaffeineCacheManagerTests.java | 64 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java index 9087df30efa3..75a89b196d6d 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java @@ -106,9 +106,16 @@ public CaffeineCacheManager(String... cacheNames) { * with no creation of further cache regions at runtime. *

Calling this with a {@code null} collection argument resets the * mode to 'dynamic', allowing for further creation of caches again. + *

Note: Switching to static mode will remove all dynamically created + * caches, while preserving custom caches registered via + * {@link #registerCustomCache(String, com.github.benmanes.caffeine.cache.Cache)}. */ public void setCacheNames(@Nullable Collection cacheNames) { if (cacheNames != null) { + // Remove all non-custom caches before setting up static caches + this.cacheMap.keySet().retainAll(this.customCacheNames); + + // Add the specified static caches for (String name : cacheNames) { this.cacheMap.put(name, createCaffeineCache(name)); } diff --git a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java index 1f4bb58c5239..6c6bf569d7ae 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/caffeine/CaffeineCacheManagerTests.java @@ -16,6 +16,8 @@ package org.springframework.cache.caffeine; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.CompletableFuture; import com.github.benmanes.caffeine.cache.CacheLoader; @@ -284,6 +286,68 @@ void setCacheNameNullRestoreDynamicMode() { assertThat(cm.getCache("someCache")).isNotNull(); } + @Test + void setCacheNameShouldRemovePreviousDynamicCaches() { + CaffeineCacheManager manager = new CaffeineCacheManager(); + + // Given: Dynamic mode with some caches + manager.getCache("dynamicCache1"); + manager.getCache("dynamicCache2"); + assertThat(manager.getCacheNames()) + .containsExactlyInAnyOrder("dynamicCache1", "dynamicCache2"); + + // When: Switch to static mode + manager.setCacheNames(Arrays.asList("staticCache1", "staticCache2")); + + // Then: Only static caches should exist + assertThat(manager.getCacheNames()) + .containsExactlyInAnyOrder("staticCache1", "staticCache2") + .as("Dynamic caches should be removed when switching to static mode"); + + // And: Dynamic caches should not be accessible + assertThat(manager.getCache("dynamicCache1")).isNull(); + assertThat(manager.getCache("dynamicCache2")).isNull(); + } + + @Test + void setCacheNamesShouldPreserveCustomCaches() { + CaffeineCacheManager manager = new CaffeineCacheManager(); + + // Given: Custom cache registered + com.github.benmanes.caffeine.cache.Cache customNativeCache = Caffeine.newBuilder().maximumSize(100).build(); + manager.registerCustomCache("customCache", customNativeCache); + + // And: Dynamic cache created + manager.getCache("dynamicCache"); + + // When: Switch to static mode + manager.setCacheNames(List.of("staticCache")); + + // Then: Custom cache preserved, dynamic cache removed + assertThat(manager.getCacheNames()) + .contains("customCache", "staticCache") + .doesNotContain("dynamicCache"); + } + + @Test + void switchingBetweenDynamicAndStaticMode() { + CaffeineCacheManager manager = new CaffeineCacheManager(); + + // Dynamic → Static + manager.getCache("dynamicCache1"); + manager.setCacheNames(List.of("staticCache1")); + assertThat(manager.getCacheNames()).containsExactly("staticCache1"); + + // Static → Dynamic + manager.setCacheNames(null); // Re-enable dynamic mode + manager.getCache("dynamicCache2"); + assertThat(manager.getCacheNames()).contains("staticCache1", "dynamicCache2"); + + // Dynamic → Static again + manager.setCacheNames(List.of("staticCache2")); + assertThat(manager.getCacheNames()).containsExactly("staticCache2"); + } + @Test void cacheLoaderUseLoadingCache() { CaffeineCacheManager cm = new CaffeineCacheManager("c1");