diff --git a/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc b/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc index eea2a0a7744..763fa8fd645 100644 --- a/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc +++ b/framework-docs/modules/ROOT/pages/integration/cache/annotations.adoc @@ -216,8 +216,8 @@ documentation of your cache provider for more details. [[cache-annotations-cacheable-reactive]] === Caching with CompletableFuture and Reactive Return Types -As of 6.1, cache annotations take `CompletableFuture` and reactive return types -into account, automatically adapting the cache interaction accordingly. +Cache annotations take `CompletableFuture` and reactive return types into account, +automatically adapting the cache interaction accordingly. For a method returning a `CompletableFuture`, the object produced by that future will be cached whenever it is complete, and the cache lookup for a cache hit will @@ -250,6 +250,10 @@ and the cache lookup for a cache hit will be retrieved as a `Flux` (backed by a public Flux findBooks(String author) {...} ---- +This collected `List` is also what an `unless` SpEL expression sees as the `#result` for +such a method (see <>); the `Flux` returned +to the caller is unaffected by this collection process. + Such `CompletableFuture` and reactive adaptation also works for synchronized caching, computing the value only once in case of a concurrent cache miss: @@ -381,10 +385,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` |=== @@ -413,8 +419,8 @@ other), such declarations should be avoided. Note also that such conditions shou on the result object (that is, the `#result` variable), as these are validated up-front to confirm the exclusion. -As of 6.1, `@CachePut` takes `CompletableFuture` and reactive return types into account, -performing the put operation whenever the produced object is available. +`@CachePut` takes `CompletableFuture` and reactive return types into account, performing +the put operation whenever the produced object is available. TIP: When `@CachePut` is combined with `@Retryable`, the retry advice is applied outermost, so each successful retry attempt updates the cache; a failed attempt does @@ -463,8 +469,13 @@ trigger, the return values are ignored (as they do not interact with the cache). not the case with `@Cacheable` which adds data to the cache or updates data in the cache 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. +`@CacheEvict` takes `CompletableFuture` and reactive return types into account, +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..41e5cff8ebc 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 @@ -28,7 +28,7 @@ import org.springframework.core.annotation.AliasFor; /** * Annotation indicating that a method (or all methods on a class) triggers a - * {@link org.springframework.cache.Cache#evict(Object) cache evict} operation. + * {@linkplain org.springframework.cache.Cache#evict(Object) cache evict} operation. * *

This annotation may be used as a meta-annotation to create custom * composed annotations with attribute overrides. @@ -77,14 +77,15 @@ 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.
  • *
  • 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.
  • * */ @@ -117,19 +118,24 @@ 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: *

    */ 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 { *

    */ @@ -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 { *

    */ @@ -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: *

    * @since 3.2 diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java index 4f0ccd85fd6..0b2b8f2ea5a 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/Cacheable.java @@ -104,8 +104,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.
  • * */ @@ -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 { *

    * @since 3.2 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 2b2bd0110ad..70c2c6dcffb 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 @@ -1088,6 +1088,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 { + + private final List contexts; + + private final List cacheValue = new ArrayList<>(); + + public CacheEvictListSubscriber(List contexts) { + this.contexts = contexts; + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Integer.MAX_VALUE); + } + @Override + public void onNext(Object o) { + this.cacheValue.add(o); + } + @Override + public void onError(Throwable t) { + this.cacheValue.clear(); + } + @Override + public void onComplete() { + performCacheEvicts(this.contexts, this.cacheValue); + } + } + + /** * Inner class to avoid a hard dependency on the Reactive Streams API at runtime. */ @@ -1169,8 +1202,16 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker public @Nullable Object processCacheEvicts(List contexts, @Nullable Object result) { ReactiveAdapter adapter = (result != null ? this.registry.getAdapter(result.getClass()) : null); if (adapter != null) { - return adapter.fromPublisher(Mono.from(adapter.toPublisher(result)) - .doOnSuccess(value -> performCacheEvicts(contexts, value))); + if (adapter.isMultiValue()) { + Flux source = Flux.from(adapter.toPublisher(result)) + .publish().refCount(2); + source.subscribe(new CacheEvictListSubscriber(contexts)); + return adapter.fromPublisher(source); + } + else { + return adapter.fromPublisher(Mono.from(adapter.toPublisher(result)) + .doOnSuccess(value -> performCacheEvicts(contexts, value))); + } } return NOT_HANDLED; } diff --git a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java index 9ea0d396613..fa867061ee4 100644 --- a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java +++ b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java @@ -56,6 +56,7 @@ import static org.mockito.Mockito.verify; * @author Phillip Webb * @author Juergen Hoeller * @author Stephane Nicoll + * @author Sam Brannen */ class CacheReproTests { @@ -312,7 +313,7 @@ class CacheReproTests { assertThat(bean.findById("tb1").collectList().block()).isEqualTo(tb); assertThat(cache.get("tb1").get()).isEqualTo(tb); - bean.clear().blockLast(); + assertThat(bean.clear().collectList().block()).containsExactly(1, 2, 3); List tb2 = bean.findById("tb1").collectList().block(); assertThat(tb2).isNotEmpty(); assertThat(tb2).isNotEqualTo(tb); @@ -705,9 +706,9 @@ class CacheReproTests { return Flux.fromIterable(item); } - @CacheEvict(cacheNames = "itemCache", allEntries = true, condition = "#result > 0") + @CacheEvict(cacheNames = "itemCache", allEntries = true, condition = "#result == {1, 2, 3}") public Flux clear() { - return Flux.just(1); + return Flux.just(1, 2, 3); } }