Revised handling of allowNullValues for asynchronous retrieval

Includes revised cacheNames javadoc and equals/hashCode for SimpleValueWrapper.

See gh-31637
This commit is contained in:
Juergen Hoeller
2023-11-22 12:22:13 +01:00
parent 5a3ad6b7c9
commit e64b81eec4
12 changed files with 147 additions and 38 deletions

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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");

View File

@@ -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;
}
}