From 26de3401023ed963d0ebde52f12c8e8d78de6404 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:04:26 +0200 Subject: [PATCH 1/2] =?UTF-8?q?Stop=20truncating=20Flux=20results=20to=20f?= =?UTF-8?q?irst=20element=20with=20@=E2=81=A0CacheEvict?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prior to this commit, ReactiveCachingHandler.processCacheEvicts() adapted every reactive return value via Mono.from(), which subscribes for only the first element and cancels the upstream Publisher. For a @⁠CacheEvict method that returns a Flux, this silently truncated the returned sequence to its first element. In addition, the `#result` variable in `condition` SpEL expressions was bound to only that first emitted element. To address that, this commit mirrors the existing multi-value handling in processPutRequest(). When the adapter reports isMultiValue(), a side Subscriber is subscribed via publish().refCount(2) that exhausts the Flux and collects its values into a List for eviction, while the original, unmodified Flux is returned to the caller. Consequently, the `#result` variable in `condition` SpEL expressions for a Flux-returning @⁠CacheEvict method is now the full List of emitted elements rather than just the first element, making it consistent with @⁠Cacheable and @⁠CachePut. This commit also improves spr14235AdaptsToReactorFlux() in CacheReproTests. Previously it exercised @⁠CacheEvict only with a single-element Flux and never asserted on the returned sequence. Now it uses a multi-element Flux and verifies that all elements are both returned to the caller and visible to the `condition` expression. Last but not least, this commit documents the aforementioned `#result`/Flux semantics in @⁠CacheEvict's Javadoc and in the reference manual, since both were previously invalid or incomplete for this scenario. Closes gh-37309 --- .../pages/integration/cache/annotations.adoc | 17 ++++--- .../cache/annotation/CacheEvict.java | 8 +++- .../cache/interceptor/CacheAspectSupport.java | 45 ++++++++++++++++++- .../cache/CacheReproTests.java | 7 +-- 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc b/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc index eea2a0a7744..276dd91e5c2 100644 --- a/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc @@ -381,10 +381,12 @@ available to the context so that you can use them for key and conditional comput | `result` | Evaluation context -| The result of the method call (the value to be cached). Only available in `unless` - expressions, `cache put` expressions (to compute the `key`), or `cache evict` - expressions (when `beforeInvocation` is `false`). For supported wrappers (such as - `Optional`), `#result` refers to the actual object, not the wrapper. +| The result of the method call (the value to be cached, or evaluated for eviction). Only + available in `unless` expressions, `cache put` expressions (`key`, `condition`, or + `unless`), or `cache evict` expressions (`key` or `condition`, when `beforeInvocation` + is `false`). For supported wrappers (such as `Optional`), `#result` refers to the + actual object, not the wrapper. For a method returning a `Flux`, `#result` refers to a + `List` containing all values collected from the `Flux`. | `#result` |=== @@ -464,7 +466,12 @@ not the case with `@Cacheable` which adds data to the cache or updates data in t and, thus, requires a result. As of 6.1, `@CacheEvict` takes `CompletableFuture` and reactive return types into account, -performing an after-invocation evict operation whenever processing has completed. +performing an after-invocation evict operation whenever processing has completed. As with +`@Cacheable` and `@CachePut`, for a method returning a `Flux`, all emitted elements are +collected into a `List` before the evict operation is performed. When `beforeInvocation` +is `false`, that same `List` is what the `condition` SpEL expression sees as the +`#result`. Either way, the `Flux` returned to the caller is unaffected by this collection +process. TIP: When `@CacheEvict` is combined with `@Retryable`, the retry advice is applied outermost, so eviction runs again on every retry attempt -- and, with diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java index a85b2fef2bf..00841d7a14a 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheEvict.java @@ -77,7 +77,8 @@ public @interface CacheEvict { *
  • {@code #result} for a reference to the result of the method invocation, which * can only be used if {@link #beforeInvocation()} is {@code false}. For supported * wrappers such as {@code Optional}, {@code #result} refers to the actual object, - * not the wrapper
  • + * not the wrapper. For a method that returns a {@code Flux}, {@code #result} refers + * to a {@code List} containing all values collected from the {@code Flux}. *
  • {@code #root.method}, {@code #root.target}, and {@code #root.caches} for * references to the {@link java.lang.reflect.Method method}, target object, and * affected cache(s) respectively.
  • @@ -123,6 +124,11 @@ public @interface CacheEvict { *

    The SpEL expression evaluates against a dedicated context that provides the * following meta-data: *