diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/caching.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/caching.adoc index 62e2734f347..5e273a4525e 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/caching.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/io/caching.adoc @@ -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`. 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]] diff --git a/module/spring-boot-cache/src/main/java/org/springframework/boot/cache/autoconfigure/CacheProperties.java b/module/spring-boot-cache/src/main/java/org/springframework/boot/cache/autoconfigure/CacheProperties.java index 1affe906c4f..a76020f9b88 100644 --- a/module/spring-boot-cache/src/main/java/org/springframework/boot/cache/autoconfigure/CacheProperties.java +++ b/module/spring-boot-cache/src/main/java/org/springframework/boot/cache/autoconfigure/CacheProperties.java @@ -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 + + } + } /** diff --git a/module/spring-boot-cache/src/main/java/org/springframework/boot/cache/autoconfigure/CaffeineCacheConfiguration.java b/module/spring-boot-cache/src/main/java/org/springframework/boot/cache/autoconfigure/CaffeineCacheConfiguration.java index 4a84f098cb6..3122c7ad70c 100644 --- a/module/spring-boot-cache/src/main/java/org/springframework/boot/cache/autoconfigure/CaffeineCacheConfiguration.java +++ b/module/spring-boot-cache/src/main/java/org/springframework/boot/cache/autoconfigure/CaffeineCacheConfiguration.java @@ -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, ObjectProvider caffeineSpec, ObjectProvider> 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 caffeine, CaffeineCacheManager cacheManager) { - String specification = cacheProperties.getCaffeine().getSpec(); + String specification = caffeineProperties.getSpec(); if (StringUtils.hasText(specification)) { cacheManager.setCacheSpecification(specification); } diff --git a/module/spring-boot-cache/src/test/java/org/springframework/boot/cache/autoconfigure/CacheAutoConfigurationTests.java b/module/spring-boot-cache/src/test/java/org/springframework/boot/cache/autoconfigure/CacheAutoConfigurationTests.java index 2c3f9d1a187..7ff66a1c622 100644 --- a/module/spring-boot-cache/src/test/java/org/springframework/boot/cache/autoconfigure/CacheAutoConfigurationTests.java +++ b/module/spring-boot-cache/src/test/java/org/springframework/boot/cache/autoconfigure/CacheAutoConfigurationTests.java @@ -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)