Revised handling of allowNullValues for asynchronous retrieval
Includes revised cacheNames javadoc and equals/hashCode for SimpleValueWrapper. See gh-31637
This commit is contained in:
@@ -140,7 +140,7 @@ public class CaffeineCache extends AbstractValueAdaptingCache {
|
||||
public CompletableFuture<?> retrieve(Object key) {
|
||||
CompletableFuture<?> result = getAsyncCache().getIfPresent(key);
|
||||
if (result != null && isAllowNullValues()) {
|
||||
result = result.handle((value, ex) -> fromStoreValue(value));
|
||||
result = result.thenApply(this::toValueWrapper);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -48,9 +48,10 @@ import org.springframework.util.ObjectUtils;
|
||||
* A {@link CaffeineSpec}-compliant expression value can also be applied
|
||||
* via the {@link #setCacheSpecification "cacheSpecification"} bean property.
|
||||
*
|
||||
* <p>Supports the {@link Cache#retrieve(Object)} and
|
||||
* <p>Supports the asynchronous {@link Cache#retrieve(Object)} and
|
||||
* {@link Cache#retrieve(Object, Supplier)} operations through Caffeine's
|
||||
* {@link AsyncCache}, when configured via {@link #setAsyncCacheMode}.
|
||||
* {@link AsyncCache}, when configured via {@link #setAsyncCacheMode},
|
||||
* with early-determined cache misses.
|
||||
*
|
||||
* <p>Requires Caffeine 3.0 or higher, as of Spring Framework 6.1.
|
||||
*
|
||||
@@ -198,6 +199,11 @@ public class CaffeineCacheManager implements CacheManager {
|
||||
* <p>By default, this cache manager builds regular native Caffeine caches.
|
||||
* To switch to async caches which can also be used through the synchronous API
|
||||
* but come with support for {@code Cache#retrieve}, set this flag to {@code true}.
|
||||
* <p>Note that while null values in the cache are tolerated in async cache mode,
|
||||
* the recommendation is to disallow null values through
|
||||
* {@link #setAllowNullValues setAllowNullValues(false)}. This makes the semantics
|
||||
* of CompletableFuture-based access simpler and optimizes retrieval performance
|
||||
* since a Caffeine-provided CompletableFuture handle does not have to get wrapped.
|
||||
* @since 6.1
|
||||
* @see Caffeine#buildAsync()
|
||||
* @see Cache#retrieve(Object)
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.support.SimpleValueWrapper;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
@@ -170,9 +171,9 @@ class CaffeineCacheManagerTests {
|
||||
assertThat(cache1.get("key3", () -> (String) null)).isNull();
|
||||
assertThat(cache1.get("key3", () -> (String) null)).isNull();
|
||||
|
||||
assertThat(cache1.retrieve("key1").join()).isEqualTo("value1");
|
||||
assertThat(cache1.retrieve("key2").join()).isEqualTo(2);
|
||||
assertThat(cache1.retrieve("key3").join()).isNull();
|
||||
assertThat(cache1.retrieve("key1").join()).isEqualTo(new SimpleValueWrapper("value1"));
|
||||
assertThat(cache1.retrieve("key2").join()).isEqualTo(new SimpleValueWrapper(2));
|
||||
assertThat(cache1.retrieve("key3").join()).isEqualTo(new SimpleValueWrapper(null));
|
||||
cache1.evict("key3");
|
||||
assertThat(cache1.retrieve("key3")).isNull();
|
||||
assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture("value3")).join())
|
||||
@@ -184,6 +185,44 @@ class CaffeineCacheManagerTests {
|
||||
assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture(null)).join()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void asyncModeWithoutNullValues() {
|
||||
CaffeineCacheManager cm = new CaffeineCacheManager();
|
||||
cm.setAsyncCacheMode(true);
|
||||
cm.setAllowNullValues(false);
|
||||
|
||||
Cache cache1 = cm.getCache("c1");
|
||||
assertThat(cache1).isInstanceOf(CaffeineCache.class);
|
||||
Cache cache1again = cm.getCache("c1");
|
||||
assertThat(cache1).isSameAs(cache1again);
|
||||
Cache cache2 = cm.getCache("c2");
|
||||
assertThat(cache2).isInstanceOf(CaffeineCache.class);
|
||||
Cache cache2again = cm.getCache("c2");
|
||||
assertThat(cache2).isSameAs(cache2again);
|
||||
Cache cache3 = cm.getCache("c3");
|
||||
assertThat(cache3).isInstanceOf(CaffeineCache.class);
|
||||
Cache cache3again = cm.getCache("c3");
|
||||
assertThat(cache3).isSameAs(cache3again);
|
||||
|
||||
cache1.put("key1", "value1");
|
||||
assertThat(cache1.get("key1").get()).isEqualTo("value1");
|
||||
cache1.put("key2", 2);
|
||||
assertThat(cache1.get("key2").get()).isEqualTo(2);
|
||||
cache1.evict("key3");
|
||||
assertThat(cache1.get("key3")).isNull();
|
||||
assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3");
|
||||
assertThat(cache1.get("key3", () -> "value3")).isEqualTo("value3");
|
||||
cache1.evict("key3");
|
||||
|
||||
assertThat(cache1.retrieve("key1").join()).isEqualTo("value1");
|
||||
assertThat(cache1.retrieve("key2").join()).isEqualTo(2);
|
||||
assertThat(cache1.retrieve("key3")).isNull();
|
||||
assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture("value3")).join())
|
||||
.isEqualTo("value3");
|
||||
assertThat(cache1.retrieve("key3", () -> CompletableFuture.completedFuture("value3")).join())
|
||||
.isEqualTo("value3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void changeCaffeineRecreateCache() {
|
||||
CaffeineCacheManager cm = new CaffeineCacheManager("c1");
|
||||
|
||||
@@ -20,7 +20,8 @@ import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -43,9 +44,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*/
|
||||
public class CaffeineReactiveCachingTests {
|
||||
|
||||
@Test
|
||||
void withCaffeineAsyncCache() {
|
||||
ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class, ReactiveCacheableService.class);
|
||||
@ParameterizedTest
|
||||
@ValueSource(classes = {AsyncCacheModeConfig.class, AsyncCacheModeConfig.class})
|
||||
void cacheHitDetermination(Class<?> configClass) {
|
||||
ApplicationContext ctx = new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class);
|
||||
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
|
||||
|
||||
Object key = new Object();
|
||||
@@ -128,12 +130,26 @@ public class CaffeineReactiveCachingTests {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableCaching
|
||||
static class Config {
|
||||
static class AsyncCacheModeConfig {
|
||||
|
||||
@Bean
|
||||
CacheManager cacheManager() {
|
||||
CaffeineCacheManager cm = new CaffeineCacheManager("first");
|
||||
cm.setAsyncCacheMode(true);
|
||||
return cm;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableCaching
|
||||
static class AsyncCacheModeWithoutNullValuesConfig {
|
||||
|
||||
@Bean
|
||||
CacheManager cacheManager() {
|
||||
CaffeineCacheManager ccm = new CaffeineCacheManager("first");
|
||||
ccm.setAsyncCacheMode(true);
|
||||
ccm.setAllowNullValues(false);
|
||||
return ccm;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user