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:
*
+ *
{@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. 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.
diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java
index 36ce7f8802e..37814657e3b 100644
--- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java
+++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java
@@ -1105,6 +1105,39 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
}
+ /**
+ * Reactive Streams Subscriber for exhausting the Flux and collecting a List
+ * to evaluate for eviction.
+ */
+ private final class CacheEvictListSubscriber implements Subscriber
*/
@@ -118,8 +118,8 @@ public @interface CacheEvict {
/**
* Spring Expression Language (SpEL) expression used for making the cache
- * eviction operation conditional. Evict that cache if the condition evaluates
- * to {@code true}.
+ * eviction operation conditional. Evicts from the cache if the condition
+ * evaluates to {@code true}.
*
Default is {@code ""}, meaning the cache eviction is always performed.
*
The SpEL expression evaluates against a dedicated context that provides the
* following meta-data:
@@ -134,8 +134,8 @@ public @interface CacheEvict {
* affected cache(s) respectively.
*
Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
- *
Method arguments can be accessed by index. For instance the second argument
- * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
+ *
Method arguments can be accessed by index. For example, the second argument
+ * can be accessed via {@code #root.args[1]}, {@code #p1}, or {@code #a1}. Arguments
* can also be accessed by name if that information is available.
*
*/
diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java
index 74f1bf55003..bee3ecb1409 100644
--- a/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java
+++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachePut.java
@@ -28,14 +28,14 @@ import org.springframework.core.annotation.AliasFor;
/**
* Annotation indicating that a method (or all methods on a class) triggers a
- * {@link org.springframework.cache.Cache#put(Object, Object) cache put} operation.
+ * {@linkplain org.springframework.cache.Cache#put(Object, Object) cache put} operation.
*
*
In contrast to the {@link Cacheable @Cacheable} annotation, this annotation
* does not cause the advised method to be skipped. Rather, it always causes the
* method to be invoked and its result to be stored in the associated cache if the
* {@link #condition()} and {@link #unless()} expressions match accordingly. Note
- * that Java8's {@code Optional} return types are automatically handled and its
- * content is stored in the cache if present.
+ * that Java's {@code Optional} return types are automatically handled and their
+ * contents are stored in the cache if present.
*
*
This annotation may be used as a meta-annotation to create custom
* composed annotations with attribute overrides.
@@ -84,14 +84,16 @@ public @interface CachePut {
*
*
{@code #result} for a reference to the result of the method invocation. For
* supported wrappers such as {@code Optional}, {@code #result} refers to the actual
- * object, not the wrapper
+ * object, 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},
+ * mirroring the value that is cached.
*
{@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.
*
Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
- *
Method arguments can be accessed by index. For instance the second argument
- * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
+ *
Method arguments can be accessed by index. For example, the second argument
+ * can be accessed via {@code #root.args[1]}, {@code #p1}, or {@code #a1}. Arguments
* can also be accessed by name if that information is available.
*
*/
@@ -124,7 +126,7 @@ public @interface CachePut {
/**
* Spring Expression Language (SpEL) expression used for making the cache
- * put operation conditional. Update the cache if the condition evaluates to
+ * put operation conditional. Updates the cache if the condition evaluates to
* {@code true}.
*
This expression is evaluated after the method has been called due to the
* nature of the put operation and can therefore refer to the {@code result}.
@@ -134,14 +136,16 @@ public @interface CachePut {
*
*
{@code #result} for a reference to the result of the method invocation. For
* supported wrappers such as {@code Optional}, {@code #result} refers to the actual
- * object, not the wrapper
+ * object, 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},
+ * mirroring the value that is cached.
*
{@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.
*
Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
- *
Method arguments can be accessed by index. For instance the second argument
- * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
+ *
Method arguments can be accessed by index. For example, the second argument
+ * can be accessed via {@code #root.args[1]}, {@code #p1}, or {@code #a1}. Arguments
* can also be accessed by name if that information is available.
*
*/
@@ -149,21 +153,23 @@ public @interface CachePut {
/**
* Spring Expression Language (SpEL) expression used to veto the cache put operation.
- * Veto updating the cache if the condition evaluates to {@code true}.
+ * Vetoes updating the cache if the condition evaluates to {@code true}.
*
Default is {@code ""}, meaning that caching is never vetoed.
*
The SpEL expression evaluates against a dedicated context that provides the
* following meta-data:
*
*
{@code #result} for a reference to the result of the method invocation. For
* supported wrappers such as {@code Optional}, {@code #result} refers to the actual
- * object, not the wrapper
+ * object, 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},
+ * mirroring the value that is cached.
*
{@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.
*
Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
- *
Method arguments can be accessed by index. For instance the second argument
- * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
+ *
Method arguments can be accessed by index. For example, the second argument
+ * can be accessed via {@code #root.args[1]}, {@code #p1}, or {@code #a1}. Arguments
* can also be accessed by name if that information is available.
Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
- *
Method arguments can be accessed by index. For instance the second argument
- * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
+ *
Method arguments can be accessed by index. For example, the second argument
+ * can be accessed via {@code #root.args[1]}, {@code #p1}, or {@code #a1}. Arguments
* can also be accessed by name if that information is available.
*
*/
@@ -138,7 +138,7 @@ public @interface Cacheable {
/**
* Spring Expression Language (SpEL) expression used for making the method
- * caching conditional. Cache the result if the condition evaluates to
+ * caching conditional. Caches the result if the condition evaluates to
* {@code true}.
*
Default is {@code ""}, meaning the method result is always cached.
*
The SpEL expression evaluates against a dedicated context that provides the
@@ -149,8 +149,8 @@ public @interface Cacheable {
* affected cache(s) respectively.
*
Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
- *
Method arguments can be accessed by index. For instance the second argument
- * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
+ *
Method arguments can be accessed by index. For example, the second argument
+ * can be accessed via {@code #root.args[1]}, {@code #p1}, or {@code #a1}. Arguments
* can also be accessed by name if that information is available.
*
*/
@@ -158,7 +158,7 @@ public @interface Cacheable {
/**
* Spring Expression Language (SpEL) expression used to veto method caching.
- * Veto caching the result if the condition evaluates to {@code true}.
+ * Vetoes caching of the result if the condition evaluates to {@code true}.
*
Unlike {@link #condition}, this expression is evaluated after the method
* has been called and can therefore refer to the {@code result}.
*
Default is {@code ""}, meaning that caching is never vetoed.
@@ -167,14 +167,16 @@ public @interface Cacheable {
*
*
{@code #result} for a reference to the result of the method invocation. For
* supported wrappers such as {@code Optional}, {@code #result} refers to the actual
- * object, not the wrapper
+ * object, 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},
+ * mirroring the value that would be cached.
*
{@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.
*
Shortcuts for the method name ({@code #root.methodName}) and target class
* ({@code #root.targetClass}) are also available.
- *
Method arguments can be accessed by index. For instance the second argument
- * can be accessed via {@code #root.args[1]}, {@code #p1} or {@code #a1}. Arguments
+ *
Method arguments can be accessed by index. For example, the second argument
+ * can be accessed via {@code #root.args[1]}, {@code #p1}, or {@code #a1}. Arguments
* can also be accessed by name if that information is available.