Skip to content

Commit dc564f3

Browse files
committed
Respect cache hit when empty Mono/Flux response is returned
Closes gh-31868
1 parent d7ce13c commit dc564f3

File tree

3 files changed

+74
-16
lines changed

3 files changed

+74
-16
lines changed

spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,11 @@ private Object findInCaches(CacheOperationContext context, Object key,
503503
private Object evaluate(@Nullable Object cacheHit, CacheOperationInvoker invoker, Method method,
504504
CacheOperationContexts contexts) {
505505

506+
// Re-invocation in reactive pipeline after late cache hit determination?
507+
if (contexts.processed) {
508+
return cacheHit;
509+
}
510+
506511
Object cacheValue;
507512
Object returnValue;
508513

@@ -541,6 +546,9 @@ private Object evaluate(@Nullable Object cacheHit, CacheOperationInvoker invoker
541546
returnValue = returnOverride;
542547
}
543548

549+
// Mark as processed for re-invocation after late cache hit determination
550+
contexts.processed = true;
551+
544552
return returnValue;
545553
}
546554

@@ -688,6 +696,8 @@ private class CacheOperationContexts {
688696

689697
private final boolean sync;
690698

699+
boolean processed;
700+
691701
public CacheOperationContexts(Collection<? extends CacheOperation> operations, Method method,
692702
Object[] args, Object target, Class<?> targetClass) {
693703

@@ -1082,21 +1092,25 @@ public Object findInCaches(CacheOperationContext context, Cache cache, Object ke
10821092
return null;
10831093
}
10841094
if (adapter.isMultiValue()) {
1085-
return adapter.fromPublisher(Flux.from(
1086-
Mono.fromFuture(cachedFuture)
1087-
.flatMap(value -> (Mono<?>) evaluate(Mono.justOrEmpty(unwrapCacheValue(value)), invoker, method, contexts)))
1088-
.flatMap(v -> (v instanceof Iterable<?> iv ? Flux.fromIterable(iv) : Flux.just(v)))
1089-
.switchIfEmpty(Flux.defer(() -> (Flux<?>) evaluate(null, invoker, method, contexts))));
1095+
return adapter.fromPublisher(Flux.from(Mono.fromFuture(cachedFuture))
1096+
.switchIfEmpty(Flux.defer(() -> (Flux) evaluate(null, invoker, method, contexts)))
1097+
.flatMap(v -> evaluate(valueToFlux(v, contexts), invoker, method, contexts)));
10901098
}
10911099
else {
10921100
return adapter.fromPublisher(Mono.fromFuture(cachedFuture)
1093-
.flatMap(value -> (Mono<?>) evaluate(Mono.justOrEmpty(unwrapCacheValue(value)), invoker, method, contexts))
1094-
.switchIfEmpty(Mono.defer(() -> (Mono) evaluate(null, invoker, method, contexts))));
1101+
.switchIfEmpty(Mono.defer(() -> (Mono) evaluate(null, invoker, method, contexts)))
1102+
.flatMap(v -> evaluate(Mono.justOrEmpty(unwrapCacheValue(v)), invoker, method, contexts)));
10951103
}
10961104
}
10971105
return NOT_HANDLED;
10981106
}
10991107

1108+
private Flux<?> valueToFlux(Object value, CacheOperationContexts contexts) {
1109+
Object data = unwrapCacheValue(value);
1110+
return (!contexts.processed && data instanceof Iterable<?> iterable ? Flux.fromIterable(iterable) :
1111+
(data != null ? Flux.just(data) : Flux.empty()));
1112+
}
1113+
11001114
@Nullable
11011115
public Object processPutRequest(CachePutRequest request, @Nullable Object result) {
11021116
ReactiveAdapter adapter = (result != null ? this.registry.getAdapter(result.getClass()) : null);

spring-context/src/test/java/org/springframework/cache/CacheReproTests.java

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,9 @@ void spr14235AdaptsToCompletableFuture() {
202202
assertThat(bean.findById("tb2").join()).isNotSameAs(tb);
203203
assertThat(cache.get("tb2")).isNull();
204204

205-
assertThat(bean.findByIdEmpty("").join()).isNull();
206205
assertThat(bean.findByIdEmpty("").join()).isNull();
207206
assertThat(cache.get("").get()).isNull();
207+
assertThat(bean.findByIdEmpty("").join()).isNull();
208208

209209
context.close();
210210
}
@@ -230,9 +230,9 @@ void spr14235AdaptsToCompletableFutureWithSync() throws Exception {
230230
assertThat(bean.findById("tb1").get()).isSameAs(tb);
231231
assertThat(cache.get("tb1").get()).isSameAs(tb);
232232

233-
assertThat(bean.findById("").join()).isNull();
234233
assertThat(bean.findById("").join()).isNull();
235234
assertThat(cache.get("").get()).isNull();
235+
assertThat(bean.findById("").join()).isNull();
236236

237237
context.close();
238238
}
@@ -265,9 +265,9 @@ void spr14235AdaptsToReactorMono() {
265265
assertThat(bean.findById("tb2").block()).isNotSameAs(tb);
266266
assertThat(cache.get("tb2")).isNull();
267267

268-
assertThat(bean.findByIdEmpty("").block()).isNull();
269268
assertThat(bean.findByIdEmpty("").block()).isNull();
270269
assertThat(cache.get("").get()).isNull();
270+
assertThat(bean.findByIdEmpty("").block()).isNull();
271271

272272
context.close();
273273
}
@@ -293,9 +293,9 @@ void spr14235AdaptsToReactorMonoWithSync() {
293293
assertThat(bean.findById("tb1").block()).isSameAs(tb);
294294
assertThat(cache.get("tb1").get()).isSameAs(tb);
295295

296-
assertThat(bean.findById("").block()).isNull();
297296
assertThat(bean.findById("").block()).isNull();
298297
assertThat(cache.get("").get()).isNull();
298+
assertThat(bean.findById("").block()).isNull();
299299

300300
context.close();
301301
}
@@ -328,9 +328,9 @@ void spr14235AdaptsToReactorFlux() {
328328
assertThat(bean.findById("tb2").collectList().block()).isNotEqualTo(tb);
329329
assertThat(cache.get("tb2")).isNull();
330330

331-
assertThat(bean.findByIdEmpty("").collectList().block()).isEmpty();
332331
assertThat(bean.findByIdEmpty("").collectList().block()).isEmpty();
333332
assertThat(cache.get("").get()).isEqualTo(Collections.emptyList());
333+
assertThat(bean.findByIdEmpty("").collectList().block()).isEmpty();
334334

335335
context.close();
336336
}
@@ -356,9 +356,9 @@ void spr14235AdaptsToReactorFluxWithSync() {
356356
assertThat(bean.findById("tb1").collectList().block()).isEqualTo(tb);
357357
assertThat(cache.get("tb1").get()).isEqualTo(tb);
358358

359-
assertThat(bean.findById("").collectList().block()).isEmpty();
360359
assertThat(bean.findById("").collectList().block()).isEmpty();
361360
assertThat(cache.get("").get()).isEqualTo(Collections.emptyList());
361+
assertThat(bean.findById("").collectList().block()).isEmpty();
362362

363363
context.close();
364364
}
@@ -587,13 +587,17 @@ public Spr14230Service service() {
587587

588588
public static class Spr14235FutureService {
589589

590+
private boolean emptyCalled;
591+
590592
@Cacheable(value = "itemCache", unless = "#result.name == 'tb2'")
591593
public CompletableFuture<TestBean> findById(String id) {
592594
return CompletableFuture.completedFuture(new TestBean(id));
593595
}
594596

595597
@Cacheable(value = "itemCache")
596598
public CompletableFuture<TestBean> findByIdEmpty(String id) {
599+
assertThat(emptyCalled).isFalse();
600+
emptyCalled = true;
597601
return CompletableFuture.completedFuture(null);
598602
}
599603

@@ -611,9 +615,16 @@ public CompletableFuture<Void> clear() {
611615

612616
public static class Spr14235FutureServiceSync {
613617

618+
private boolean emptyCalled;
619+
614620
@Cacheable(value = "itemCache", sync = true)
615621
public CompletableFuture<TestBean> findById(String id) {
616-
return CompletableFuture.completedFuture(id.isEmpty() ? null : new TestBean(id));
622+
if (id.isEmpty()) {
623+
assertThat(emptyCalled).isFalse();
624+
emptyCalled = true;
625+
return CompletableFuture.completedFuture(null);
626+
}
627+
return CompletableFuture.completedFuture(new TestBean(id));
617628
}
618629

619630
@CachePut(cacheNames = "itemCache", key = "#item.name")
@@ -625,13 +636,17 @@ public TestBean insertItem(TestBean item) {
625636

626637
public static class Spr14235MonoService {
627638

639+
private boolean emptyCalled;
640+
628641
@Cacheable(value = "itemCache", unless = "#result.name == 'tb2'")
629642
public Mono<TestBean> findById(String id) {
630643
return Mono.just(new TestBean(id));
631644
}
632645

633646
@Cacheable(value = "itemCache")
634647
public Mono<TestBean> findByIdEmpty(String id) {
648+
assertThat(emptyCalled).isFalse();
649+
emptyCalled = true;
635650
return Mono.empty();
636651
}
637652

@@ -649,9 +664,16 @@ public Mono<Void> clear() {
649664

650665
public static class Spr14235MonoServiceSync {
651666

667+
private boolean emptyCalled;
668+
652669
@Cacheable(value = "itemCache", sync = true)
653670
public Mono<TestBean> findById(String id) {
654-
return (id.isEmpty() ? Mono.empty() : Mono.just(new TestBean(id)));
671+
if (id.isEmpty()) {
672+
assertThat(emptyCalled).isFalse();
673+
emptyCalled = true;
674+
return Mono.empty();
675+
}
676+
return Mono.just(new TestBean(id));
655677
}
656678

657679
@CachePut(cacheNames = "itemCache", key = "#item.name")
@@ -665,13 +687,17 @@ public static class Spr14235FluxService {
665687

666688
private int counter = 0;
667689

690+
private boolean emptyCalled;
691+
668692
@Cacheable(value = "itemCache", unless = "#result[0].name == 'tb2'")
669693
public Flux<TestBean> findById(String id) {
670694
return Flux.just(new TestBean(id), new TestBean(id + (counter++)));
671695
}
672696

673697
@Cacheable(value = "itemCache")
674698
public Flux<TestBean> findByIdEmpty(String id) {
699+
assertThat(emptyCalled).isFalse();
700+
emptyCalled = true;
675701
return Flux.empty();
676702
}
677703

@@ -691,9 +717,16 @@ public static class Spr14235FluxServiceSync {
691717

692718
private int counter = 0;
693719

720+
private boolean emptyCalled;
721+
694722
@Cacheable(value = "itemCache", sync = true)
695723
public Flux<TestBean> findById(String id) {
696-
return (id.isEmpty() ? Flux.empty() : Flux.just(new TestBean(id), new TestBean(id + (counter++))));
724+
if (id.isEmpty()) {
725+
assertThat(emptyCalled).isFalse();
726+
emptyCalled = true;
727+
return Flux.empty();
728+
}
729+
return Flux.just(new TestBean(id), new TestBean(id + (counter++)));
697730
}
698731

699732
@CachePut(cacheNames = "itemCache", key = "#id")

spring-context/src/test/java/org/springframework/cache/annotation/ReactiveCachingTests.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
3333
import org.springframework.context.annotation.Bean;
3434
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.lang.Nullable;
3536

3637
import static org.assertj.core.api.Assertions.assertThat;
3738

@@ -171,6 +172,11 @@ protected Cache createConcurrentMapCache(String name) {
171172
public CompletableFuture<?> retrieve(Object key) {
172173
return CompletableFuture.completedFuture(lookup(key));
173174
}
175+
@Override
176+
public void put(Object key, @Nullable Object value) {
177+
assertThat(get(key) == null).as("Double put");
178+
super.put(key, value);
179+
}
174180
};
175181
}
176182
};
@@ -193,6 +199,11 @@ public CompletableFuture<?> retrieve(Object key) {
193199
Object value = lookup(key);
194200
return CompletableFuture.completedFuture(value != null ? toValueWrapper(value) : null);
195201
}
202+
@Override
203+
public void put(Object key, @Nullable Object value) {
204+
assertThat(get(key) == null).as("Double put");
205+
super.put(key, value);
206+
}
196207
};
197208
}
198209
};

0 commit comments

Comments
 (0)