Add caching annotation support for CompletableFuture and reactive return values

Includes CompletableFuture-based retrieve operations on Spring's Cache interface.
Includes support for retrieve operations on CaffeineCache and ConcurrentMapCache.
Includes async cache mode option on CaffeineCacheManager.

Closes gh-17559
Closes gh-17920
Closes gh-30122
This commit is contained in:
Juergen Hoeller
2023-07-21 20:27:23 +02:00
parent d65d285378
commit f99faac073
9 changed files with 952 additions and 145 deletions

View File

@@ -20,11 +20,15 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
@@ -118,6 +122,7 @@ class CacheReproTests {
assertThat(cacheResolver.getCache("foo").get("foo")).isNull();
Object result = bean.getSimple("foo"); // cache name = id
assertThat(cacheResolver.getCache("foo").get("foo").get()).isEqualTo(result);
context.close();
}
@@ -127,7 +132,7 @@ class CacheReproTests {
Spr13081Service bean = context.getBean(Spr13081Service.class);
assertThatIllegalStateException().isThrownBy(() -> bean.getSimple(null))
.withMessageContaining(MyCacheResolver.class.getName());
.withMessageContaining(MyCacheResolver.class.getName());
context.close();
}
@@ -146,6 +151,7 @@ class CacheReproTests {
TestBean tb2 = bean.findById("tb1").get();
assertThat(tb2).isNotSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb2);
context.close();
}
@@ -164,6 +170,151 @@ class CacheReproTests {
TestBean tb2 = bean.findById("tb1").get();
assertThat(tb2).isNotSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb2);
context.close();
}
@Test
void spr14235AdaptsToCompletableFuture() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(Spr14235Config.class, Spr14235FutureService.class);
Spr14235FutureService bean = context.getBean(Spr14235FutureService.class);
Cache cache = context.getBean(CacheManager.class).getCache("itemCache");
TestBean tb = bean.findById("tb1").join();
assertThat(bean.findById("tb1").join()).isSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb);
bean.clear().join();
TestBean tb2 = bean.findById("tb1").join();
assertThat(tb2).isNotSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb2);
bean.clear().join();
bean.insertItem(tb).join();
assertThat(bean.findById("tb1").join()).isSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb);
context.close();
}
@Test
void spr14235AdaptsToCompletableFutureWithSync() throws Exception {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(Spr14235Config.class, Spr14235FutureServiceSync.class);
Spr14235FutureServiceSync bean = context.getBean(Spr14235FutureServiceSync.class);
Cache cache = context.getBean(CacheManager.class).getCache("itemCache");
TestBean tb = bean.findById("tb1").get();
assertThat(bean.findById("tb1").get()).isSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb);
cache.clear();
TestBean tb2 = bean.findById("tb1").get();
assertThat(tb2).isNotSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb2);
cache.clear();
bean.insertItem(tb);
assertThat(bean.findById("tb1").get()).isSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb);
context.close();
}
@Test
void spr14235AdaptsToReactorMono() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(Spr14235Config.class, Spr14235MonoService.class);
Spr14235MonoService bean = context.getBean(Spr14235MonoService.class);
Cache cache = context.getBean(CacheManager.class).getCache("itemCache");
TestBean tb = bean.findById("tb1").block();
assertThat(bean.findById("tb1").block()).isSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb);
bean.clear().block();
TestBean tb2 = bean.findById("tb1").block();
assertThat(tb2).isNotSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb2);
bean.clear().block();
bean.insertItem(tb).block();
assertThat(bean.findById("tb1").block()).isSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb);
context.close();
}
@Test
void spr14235AdaptsToReactorMonoWithSync() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(Spr14235Config.class, Spr14235MonoServiceSync.class);
Spr14235MonoServiceSync bean = context.getBean(Spr14235MonoServiceSync.class);
Cache cache = context.getBean(CacheManager.class).getCache("itemCache");
TestBean tb = bean.findById("tb1").block();
assertThat(bean.findById("tb1").block()).isSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb);
cache.clear();
TestBean tb2 = bean.findById("tb1").block();
assertThat(tb2).isNotSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb2);
cache.clear();
bean.insertItem(tb);
assertThat(bean.findById("tb1").block()).isSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb);
context.close();
}
@Test
void spr14235AdaptsToReactorFlux() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(Spr14235Config.class, Spr14235FluxService.class);
Spr14235FluxService bean = context.getBean(Spr14235FluxService.class);
Cache cache = context.getBean(CacheManager.class).getCache("itemCache");
List<TestBean> tb = bean.findById("tb1").collectList().block();
assertThat(bean.findById("tb1").collectList().block()).isEqualTo(tb);
assertThat(cache.get("tb1").get()).isEqualTo(tb);
bean.clear().blockLast();
List<TestBean> tb2 = bean.findById("tb1").collectList().block();
assertThat(tb2).isNotEqualTo(tb);
assertThat(cache.get("tb1").get()).isEqualTo(tb2);
bean.clear().blockLast();
bean.insertItem("tb1", tb).blockLast();
assertThat(bean.findById("tb1").collectList().block()).isEqualTo(tb);
assertThat(cache.get("tb1").get()).isEqualTo(tb);
context.close();
}
@Test
void spr14235AdaptsToReactorFluxWithSync() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(Spr14235Config.class, Spr14235FluxServiceSync.class);
Spr14235FluxServiceSync bean = context.getBean(Spr14235FluxServiceSync.class);
Cache cache = context.getBean(CacheManager.class).getCache("itemCache");
List<TestBean> tb = bean.findById("tb1").collectList().block();
assertThat(bean.findById("tb1").collectList().block()).isEqualTo(tb);
assertThat(cache.get("tb1").get()).isEqualTo(tb);
cache.clear();
List<TestBean> tb2 = bean.findById("tb1").collectList().block();
assertThat(tb2).isNotEqualTo(tb);
assertThat(cache.get("tb1").get()).isEqualTo(tb2);
cache.clear();
bean.insertItem("tb1", tb);
assertThat(bean.findById("tb1").collectList().block()).isEqualTo(tb);
assertThat(cache.get("tb1").get()).isEqualTo(tb);
context.close();
}
@@ -177,6 +328,7 @@ class CacheReproTests {
bean.insertItem(tb);
assertThat(bean.findById("tb1").get()).isSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb);
context.close();
}
@@ -190,6 +342,7 @@ class CacheReproTests {
bean.insertItem(tb);
assertThat(bean.findById("tb1").get()).isSameAs(tb);
assertThat(cache.get("tb1").get()).isSameAs(tb);
context.close();
}
@@ -387,6 +540,120 @@ class CacheReproTests {
}
public static class Spr14235FutureService {
@Cacheable(value = "itemCache")
public CompletableFuture<TestBean> findById(String id) {
return CompletableFuture.completedFuture(new TestBean(id));
}
@CachePut(cacheNames = "itemCache", key = "#item.name")
public CompletableFuture<TestBean> insertItem(TestBean item) {
return CompletableFuture.completedFuture(item);
}
@CacheEvict(cacheNames = "itemCache", allEntries = true)
public CompletableFuture<Void> clear() {
return CompletableFuture.completedFuture(null);
}
}
public static class Spr14235FutureServiceSync {
@Cacheable(value = "itemCache", sync = true)
public CompletableFuture<TestBean> findById(String id) {
return CompletableFuture.completedFuture(new TestBean(id));
}
@CachePut(cacheNames = "itemCache", key = "#item.name")
public TestBean insertItem(TestBean item) {
return item;
}
}
public static class Spr14235MonoService {
@Cacheable(value = "itemCache")
public Mono<TestBean> findById(String id) {
return Mono.just(new TestBean(id));
}
@CachePut(cacheNames = "itemCache", key = "#item.name")
public Mono<TestBean> insertItem(TestBean item) {
return Mono.just(item);
}
@CacheEvict(cacheNames = "itemCache", allEntries = true)
public Mono<Void> clear() {
return Mono.empty();
}
}
public static class Spr14235MonoServiceSync {
@Cacheable(value = "itemCache", sync = true)
public Mono<TestBean> findById(String id) {
return Mono.just(new TestBean(id));
}
@CachePut(cacheNames = "itemCache", key = "#item.name")
public TestBean insertItem(TestBean item) {
return item;
}
}
public static class Spr14235FluxService {
private int counter = 0;
@Cacheable(value = "itemCache")
public Flux<TestBean> findById(String id) {
return Flux.just(new TestBean(id), new TestBean(id + (counter++)));
}
@CachePut(cacheNames = "itemCache", key = "#id")
public Flux<TestBean> insertItem(String id, List<TestBean> item) {
return Flux.fromIterable(item);
}
@CacheEvict(cacheNames = "itemCache", allEntries = true)
public Flux<Void> clear() {
return Flux.empty();
}
}
public static class Spr14235FluxServiceSync {
private int counter = 0;
@Cacheable(value = "itemCache", sync = true)
public Flux<TestBean> findById(String id) {
return Flux.just(new TestBean(id), new TestBean(id + (counter++)));
}
@CachePut(cacheNames = "itemCache", key = "#id")
public List<TestBean> insertItem(String id, List<TestBean> item) {
return item;
}
}
@Configuration
@EnableCaching
public static class Spr14235Config {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager();
}
}
public static class Spr14853Service {
@Cacheable(value = "itemCache", sync = true)