Merge pull request #51844 from hunseonglee

Closes gh-51844

* caffeine-async-cache-mode:
  Polish "Add property to enable Caffeine async cache mode"
  Add property to enable Caffeine async cache mode
This commit is contained in:
Stéphane Nicoll
2026-09-28 10:46:11 +02:00
4 changed files with 66 additions and 3 deletions
@@ -223,6 +223,11 @@ If a javadoc:com.github.benmanes.caffeine.cache.CacheLoader[] bean is defined, i
Since the javadoc:com.github.benmanes.caffeine.cache.CacheLoader[] is going to be associated with _all_ caches managed by the cache manager, it must be defined as `CacheLoader<Object, Object>`.
The auto-configuration ignores any other generic type.
To enable the use of `Cache#retrieve`, an javadoc:com.github.benmanes.caffeine.cache.AsyncCache[] should be available.
Such caches can be created explicitly with a `CacheManagerCustomizer` bean that calls `CaffeineCacheManager#registerCustomCache`.
Alternatively, named caches and caches on demand can be async by setting configprop:spring.cache.caffeine.cache-mode[] to `async`.
Regular, native, caches can still be configured explicitly using a customizer bean.
[[io.caching.provider.cache2k]]
@@ -116,12 +116,25 @@ public class CacheProperties {
*/
public static class Caffeine {
/**
* Mode to use for configured cache names or caches created on demand.
*/
private CacheMode cacheMode = CacheMode.NATIVE;
/**
* The spec to use to create caches. See CaffeineSpec for more details on the spec
* format.
*/
private @Nullable String spec;
public CacheMode getCacheMode() {
return this.cacheMode;
}
public void setCacheMode(CacheMode cacheMode) {
this.cacheMode = cacheMode;
}
public @Nullable String getSpec() {
return this.spec;
}
@@ -130,6 +143,21 @@ public class CacheProperties {
this.spec = spec;
}
public enum CacheMode {
/**
* Use regular caches backed by Caffeine's Cache.
*/
NATIVE,
/**
* Use asynchronous caches backed by Caffeine's AsyncCache, adding support for
* Cache.retrieve.
*/
ASYNC
}
}
/**
@@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.cache.autoconfigure.CacheProperties.Caffeine.CacheMode;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
@@ -61,14 +62,18 @@ class CaffeineCacheConfiguration {
ObjectProvider<Caffeine<Object, Object>> caffeine, ObjectProvider<CaffeineSpec> caffeineSpec,
ObjectProvider<CacheLoader<Object, Object>> cacheLoader) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
setCacheBuilder(cacheProperties, caffeineSpec.getIfAvailable(), caffeine.getIfAvailable(), cacheManager);
CacheProperties.Caffeine caffeineProperties = cacheProperties.getCaffeine();
if (caffeineProperties.getCacheMode() == CacheMode.ASYNC) {
cacheManager.setAsyncCacheMode(true);
}
setCacheBuilder(caffeineProperties, caffeineSpec.getIfAvailable(), caffeine.getIfAvailable(), cacheManager);
cacheLoader.ifAvailable(cacheManager::setCacheLoader);
return cacheManager;
}
private void setCacheBuilder(CacheProperties cacheProperties, @Nullable CaffeineSpec caffeineSpec,
private void setCacheBuilder(CacheProperties.Caffeine caffeineProperties, @Nullable CaffeineSpec caffeineSpec,
@Nullable Caffeine<Object, Object> caffeine, CaffeineCacheManager cacheManager) {
String specification = cacheProperties.getCaffeine().getSpec();
String specification = caffeineProperties.getSpec();
if (StringUtils.hasText(specification)) {
cacheManager.setCacheSpecification(specification);
}
@@ -80,6 +80,8 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
@@ -813,6 +815,29 @@ class CacheAutoConfigurationTests extends AbstractCacheAutoConfigurationTests {
.run(this::validateCaffeineCacheWithStats);
}
@Test
void caffeineCacheUsesNativeModeByDefault() {
this.contextRunner.withUserConfiguration(DefaultCacheConfiguration.class)
.withPropertyValues("spring.cache.type=caffeine", "spring.cache.cacheNames=foo")
.run((context) -> {
CaffeineCacheManager manager = getCacheManager(context, CaffeineCacheManager.class);
assertThat(manager.getCache("foo")).isInstanceOfSatisfying(CaffeineCache.class,
(cache) -> assertThatIllegalStateException().isThrownBy(cache::getAsyncCache));
});
}
@Test
void caffeineCacheWithAsyncCacheMode() {
this.contextRunner.withUserConfiguration(DefaultCacheConfiguration.class)
.withPropertyValues("spring.cache.type=caffeine", "spring.cache.caffeine.cache-mode=async",
"spring.cache.cacheNames=foo")
.run((context) -> {
CaffeineCacheManager manager = getCacheManager(context, CaffeineCacheManager.class);
assertThat(manager.getCache("foo")).isInstanceOfSatisfying(CaffeineCache.class,
(cache) -> assertThatNoException().isThrownBy(cache::getAsyncCache));
});
}
@Test
void autoConfiguredCacheManagerCanBeSwapped() {
this.contextRunner.withUserConfiguration(CacheManagerPostProcessorConfiguration.class)