• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

3.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.01
today-context/src/main/java/infra/cache/interceptor/CacheAspectSupport.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.cache.interceptor;
19

20
import org.jspecify.annotations.Nullable;
21
import org.reactivestreams.Subscriber;
22
import org.reactivestreams.Subscription;
23

24
import java.lang.reflect.Method;
25
import java.lang.reflect.Proxy;
26
import java.util.ArrayList;
27
import java.util.Collection;
28
import java.util.Collections;
29
import java.util.List;
30
import java.util.Optional;
31
import java.util.concurrent.CompletableFuture;
32
import java.util.concurrent.CompletionStage;
33
import java.util.concurrent.ConcurrentHashMap;
34
import java.util.concurrent.atomic.AtomicBoolean;
35
import java.util.function.Supplier;
36

37
import infra.aop.framework.AopProxyUtils;
38
import infra.aop.support.AopUtils;
39
import infra.beans.factory.BeanFactory;
40
import infra.beans.factory.BeanFactoryAware;
41
import infra.beans.factory.InitializingBean;
42
import infra.beans.factory.NoSuchBeanDefinitionException;
43
import infra.beans.factory.NoUniqueBeanDefinitionException;
44
import infra.beans.factory.SmartInitializingSingleton;
45
import infra.beans.factory.annotation.BeanFactoryAnnotationUtils;
46
import infra.beans.factory.config.ConfigurableBeanFactory;
47
import infra.cache.Cache;
48
import infra.cache.CacheManager;
49
import infra.cache.annotation.Cacheable;
50
import infra.context.expression.AnnotatedElementKey;
51
import infra.context.expression.BeanFactoryResolver;
52
import infra.core.BridgeMethodResolver;
53
import infra.core.ReactiveAdapter;
54
import infra.core.ReactiveAdapterRegistry;
55
import infra.core.ReactiveStreams;
56
import infra.expression.EvaluationContext;
57
import infra.expression.spel.support.StandardEvaluationContext;
58
import infra.lang.Assert;
59
import infra.lang.TodayStrategies;
60
import infra.logging.Logger;
61
import infra.logging.LoggerFactory;
62
import infra.util.ClassUtils;
63
import infra.util.CollectionUtils;
64
import infra.util.MultiValueMap;
65
import infra.util.ObjectUtils;
66
import infra.util.ReflectionUtils;
67
import infra.util.StringUtils;
68
import infra.util.concurrent.Future;
69
import infra.util.function.SingletonSupplier;
70
import infra.util.function.SupplierUtils;
71
import reactor.core.publisher.Flux;
72
import reactor.core.publisher.Mono;
73

74
/**
75
 * Base class for caching aspects, such as the {@link CacheInterceptor} or an
76
 * AspectJ aspect.
77
 *
78
 * <p>This enables the underlying Framework caching infrastructure to be used easily
79
 * to implement an aspect for any aspect system.
80
 *
81
 * <p>Subclasses are responsible for calling relevant methods in the correct order.
82
 *
83
 * <p>Uses the <b>Strategy</b> design pattern. A {@link CacheOperationSource} is
84
 * used for determining caching operations, a {@link KeyGenerator} will build the
85
 * cache keys, and a {@link CacheResolver} will resolve the actual cache(s) to use.
86
 *
87
 * <p>Note: A cache aspect is serializable but does not perform any actual caching
88
 * after deserialization.
89
 *
90
 * @author Costin Leau
91
 * @author Juergen Hoeller
92
 * @author Chris Beams
93
 * @author Phillip Webb
94
 * @author Sam Brannen
95
 * @author Stephane Nicoll
96
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
97
 * @since 4.0
98
 */
99
public abstract class CacheAspectSupport extends AbstractCacheInvoker
2✔
100
        implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
101

102
  private static final Logger log = LoggerFactory.getLogger(CacheAspectSupport.class);
3✔
103

104
  /**
105
   * System property that instructs Infra caching infrastructure to ignore the
106
   * presence of Reactive Streams, in particular Reactor's {@link Mono}/{@link Flux}
107
   * in {@link Cacheable} method return type
108
   * declarations.
109
   * <p>By default Reactive Streams Publishers such as Reactor's
110
   * {@link Mono}/{@link Flux} will be specifically processed for asynchronous
111
   * caching of their produced values rather than trying to cache the returned
112
   * {@code Publisher} instances themselves.
113
   * <p>Switch this flag to "true" in order to ignore Reactive Streams Publishers and
114
   * process them as regular return values through synchronous caching, restoring 6.0
115
   * behavior. Note that this is not recommended and only works in very limited
116
   * scenarios, e.g. with manual {@code Mono.cache()}/{@code Flux.cache()} calls.
117
   *
118
   * @see org.reactivestreams.Publisher
119
   */
120
  public static final String IGNORE_REACTIVESTREAMS_PROPERTY_NAME = "infra.cache.reactivestreams.ignore";
121

122
  private static final boolean shouldIgnoreReactiveStreams =
2✔
123
          TodayStrategies.getFlag(IGNORE_REACTIVESTREAMS_PROPERTY_NAME);
2✔
124

125
  private final ConcurrentHashMap<CacheOperationCacheKey, CacheOperationMetadata> metadataCache = new ConcurrentHashMap<>(1024);
6✔
126

127
  private final StandardEvaluationContext sharedContext = new StandardEvaluationContext();
5✔
128

129
  private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator(sharedContext);
7✔
130

131
  @Nullable
1✔
132
  private final ReactiveCachingHandler reactiveCachingHandler =
133
          ReactiveStreams.isPresent && !shouldIgnoreReactiveStreams ? new ReactiveCachingHandler() : null;
10!
134

135
  @Nullable
136
  private CacheOperationSource cacheOperationSource;
137

138
  private SingletonSupplier<KeyGenerator> keyGenerator = SingletonSupplier.from(SimpleKeyGenerator::new);
4✔
139

140
  @Nullable
141
  private SingletonSupplier<CacheResolver> cacheResolver;
142

143
  @Nullable
144
  private BeanFactory beanFactory;
145

146
  private boolean initialized = false;
4✔
147

148
  /**
149
   * Configure this aspect with the given error handler, key generator and cache resolver/manager
150
   * suppliers, applying the corresponding default if a supplier is not resolvable.
151
   */
152
  public void configure(@Nullable Supplier<CacheErrorHandler> errorHandler, @Nullable Supplier<KeyGenerator> keyGenerator,
153
          @Nullable Supplier<CacheResolver> cacheResolver, @Nullable Supplier<CacheManager> cacheManager) {
154

155
    this.keyGenerator = new SingletonSupplier<>(keyGenerator, SimpleKeyGenerator::new);
7✔
156
    this.errorHandler = new SingletonSupplier<>(errorHandler, SimpleCacheErrorHandler::new);
7✔
157
    this.cacheResolver = new SingletonSupplier<>(cacheResolver,
8✔
158
            () -> SimpleCacheResolver.of(SupplierUtils.resolve(cacheManager)));
5✔
159
  }
1✔
160

161
  /**
162
   * Set one or more cache operation sources which are used to find the cache
163
   * attributes. If more than one source is provided, they will be aggregated
164
   * using a {@link CompositeCacheOperationSource}.
165
   *
166
   * @see #setCacheOperationSource
167
   */
168
  public void setCacheOperationSources(CacheOperationSource... cacheOperationSources) {
169
    Assert.notEmpty(cacheOperationSources, "At least 1 CacheOperationSource needs to be specified");
3✔
170
    this.cacheOperationSource = cacheOperationSources.length > 1 ?
5✔
171
            new CompositeCacheOperationSource(cacheOperationSources) :
5✔
172
            cacheOperationSources[0];
4✔
173
  }
1✔
174

175
  /**
176
   * Set the CacheOperationSource for this cache aspect.
177
   *
178
   * @see #setCacheOperationSources
179
   */
180
  public void setCacheOperationSource(@Nullable CacheOperationSource cacheOperationSource) {
181
    this.cacheOperationSource = cacheOperationSource;
3✔
182
  }
1✔
183

184
  /**
185
   * Return the CacheOperationSource for this cache aspect.
186
   */
187
  @Nullable
188
  public CacheOperationSource getCacheOperationSource() {
189
    return this.cacheOperationSource;
3✔
190
  }
191

192
  /**
193
   * Set the default {@link KeyGenerator} that this cache aspect should delegate to
194
   * if no specific key generator has been set for the operation.
195
   * <p>The default is a {@link SimpleKeyGenerator}.
196
   */
197
  public void setKeyGenerator(KeyGenerator keyGenerator) {
198
    this.keyGenerator = SingletonSupplier.valueOf(keyGenerator);
4✔
199
  }
1✔
200

201
  /**
202
   * Return the default {@link KeyGenerator} that this cache aspect delegates to.
203
   */
204
  public KeyGenerator getKeyGenerator() {
205
    return this.keyGenerator.obtain();
5✔
206
  }
207

208
  /**
209
   * Set the default {@link CacheResolver} that this cache aspect should delegate
210
   * to if no specific cache resolver has been set for the operation.
211
   * <p>The default resolver resolves the caches against their names and the
212
   * default cache manager.
213
   *
214
   * @see #setCacheManager
215
   * @see SimpleCacheResolver
216
   */
217
  public void setCacheResolver(@Nullable CacheResolver cacheResolver) {
218
    this.cacheResolver = SingletonSupplier.ofNullable(cacheResolver);
4✔
219
  }
1✔
220

221
  /**
222
   * Return the default {@link CacheResolver} that this cache aspect delegates to.
223
   */
224
  @Nullable
225
  public CacheResolver getCacheResolver() {
226
    return SupplierUtils.resolve(this.cacheResolver);
5✔
227
  }
228

229
  /**
230
   * Set the {@link CacheManager} to use to create a default {@link CacheResolver}.
231
   * Replace the current {@link CacheResolver}, if any.
232
   *
233
   * @see #setCacheResolver
234
   * @see SimpleCacheResolver
235
   */
236
  public void setCacheManager(CacheManager cacheManager) {
237
    this.cacheResolver = SingletonSupplier.valueOf(new SimpleCacheResolver(cacheManager));
7✔
238
  }
1✔
239

240
  /**
241
   * Set the containing {@link BeanFactory} for {@link CacheManager} and other
242
   * service lookups.
243
   */
244
  @Override
245
  public void setBeanFactory(BeanFactory beanFactory) {
246
    this.beanFactory = beanFactory;
3✔
247
    this.sharedContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
7✔
248
  }
1✔
249

250
  @Override
251
  public void afterPropertiesSet() {
252
    Assert.state(getCacheOperationSource() != null, "The 'cacheOperationSources' property is required: " +
7!
253
            "If there are no cacheable methods, then don't use a cache aspect.");
254
  }
1✔
255

256
  @Override
257
  public void afterSingletonsInstantiated(ConfigurableBeanFactory beanFactory) {
258
    if (getCacheResolver() == null) {
3✔
259
      // Lazily initialize cache resolver via default cache manager...
260
      try {
261
        setCacheManager(beanFactory.getBean(CacheManager.class));
6✔
262
      }
263
      catch (NoUniqueBeanDefinitionException ex) {
1✔
264
        int numberOfBeansFound = ex.getNumberOfBeansFound();
3✔
265
        Collection<String> beanNamesFound = ex.getBeanNamesFound();
3✔
266

267
        StringBuilder message = new StringBuilder("no CacheResolver specified and expected single matching CacheManager but found ");
5✔
268
        message.append(numberOfBeansFound);
4✔
269
        if (beanNamesFound != null) {
2!
270
          message.append(": ").append(StringUtils.collectionToCommaDelimitedString(beanNamesFound));
7✔
271
        }
272
        String exceptionMessage = message.toString();
3✔
273

274
        if (beanNamesFound != null) {
2!
275
          throw new NoUniqueBeanDefinitionException(CacheManager.class, beanNamesFound, exceptionMessage);
7✔
276
        }
277
        else {
278
          throw new NoUniqueBeanDefinitionException(CacheManager.class, numberOfBeansFound, exceptionMessage);
×
279
        }
280
      }
281
      catch (NoSuchBeanDefinitionException ex) {
1✔
282
        throw new NoSuchBeanDefinitionException(CacheManager.class, "no CacheResolver specified - "
6✔
283
                + "register a CacheManager bean or remove the @EnableCaching annotation from your configuration.");
284
      }
1✔
285
    }
286
    this.initialized = true;
3✔
287
  }
1✔
288

289
  /**
290
   * Convenience method to return a String representation of this Method
291
   * for use in logging. Can be overridden in subclasses to provide a
292
   * different identifier for the given method.
293
   *
294
   * @param method the method we're interested in
295
   * @param targetClass class the method is on
296
   * @return log message identifying this method
297
   * @see ClassUtils#getQualifiedMethodName
298
   */
299
  protected String methodIdentification(Method method, Class<?> targetClass) {
300
    Method specificMethod = ReflectionUtils.getMostSpecificMethod(method, targetClass);
×
301
    return ClassUtils.getQualifiedMethodName(specificMethod);
×
302
  }
303

304
  protected Collection<? extends Cache> getCaches(CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver) {
305
    Collection<? extends Cache> caches = cacheResolver.resolveCaches(context);
4✔
306
    if (caches.isEmpty()) {
3✔
307
      throw new IllegalStateException(
8✔
308
              "No cache could be resolved for '%s' using resolver '%s'. At least one cache should be provided per cache operation."
309
                      .formatted(context.getOperation(), cacheResolver));
9✔
310
    }
311
    return caches;
2✔
312
  }
313

314
  protected CacheOperationContext getOperationContext(CacheOperation operation,
315
          Method method, Object[] args, Object target, Class<?> targetClass) {
316

317
    CacheOperationMetadata metadata = getCacheOperationMetadata(operation, method, targetClass);
6✔
318
    return new CacheOperationContext(metadata, args, target);
8✔
319
  }
320

321
  /**
322
   * Return the {@link CacheOperationMetadata} for the specified operation.
323
   * <p>Resolve the {@link CacheResolver} and the {@link KeyGenerator} to be
324
   * used for the operation.
325
   *
326
   * @param operation the operation
327
   * @param method the method on which the operation is invoked
328
   * @param targetClass the target type
329
   * @return the resolved metadata for the operation
330
   */
331
  protected CacheOperationMetadata getCacheOperationMetadata(CacheOperation operation, Method method, Class<?> targetClass) {
332

333
    CacheOperationCacheKey cacheKey = new CacheOperationCacheKey(operation, method, targetClass);
7✔
334
    CacheOperationMetadata metadata = this.metadataCache.get(cacheKey);
6✔
335
    if (metadata == null) {
2✔
336
      KeyGenerator operationKeyGenerator;
337
      if (StringUtils.hasText(operation.getKeyGenerator())) {
4✔
338
        operationKeyGenerator = getBean(operation.getKeyGenerator(), KeyGenerator.class);
8✔
339
      }
340
      else {
341
        operationKeyGenerator = getKeyGenerator();
3✔
342
      }
343
      CacheResolver operationCacheResolver;
344
      if (StringUtils.hasText(operation.getCacheResolver())) {
4✔
345
        operationCacheResolver = getBean(operation.getCacheResolver(), CacheResolver.class);
8✔
346
      }
347
      else if (StringUtils.hasText(operation.getCacheManager())) {
4✔
348
        CacheManager cacheManager = getBean(operation.getCacheManager(), CacheManager.class);
7✔
349
        operationCacheResolver = new SimpleCacheResolver(cacheManager);
5✔
350
      }
1✔
351
      else {
352
        operationCacheResolver = getCacheResolver();
3✔
353
        Assert.state(operationCacheResolver != null, "No CacheResolver/CacheManager set");
6!
354
      }
355
      metadata = new CacheOperationMetadata(operation, method, targetClass,
9✔
356
              operationKeyGenerator, operationCacheResolver);
357
      this.metadataCache.put(cacheKey, metadata);
6✔
358
    }
359
    return metadata;
2✔
360
  }
361

362
  /**
363
   * Retrieve a bean with the specified name and type.
364
   * Used to resolve services that are referenced by name in a {@link CacheOperation}.
365
   *
366
   * @param name the name of the bean, as defined by the cache operation
367
   * @param serviceType the type expected by the operation's service reference
368
   * @return the bean matching the expected type, qualified by the given name
369
   * @throws NoSuchBeanDefinitionException if such bean does not exist
370
   * @see CacheOperation#getKeyGenerator()
371
   * @see CacheOperation#getCacheManager()
372
   * @see CacheOperation#getCacheResolver()
373
   */
374
  protected <T> T getBean(String name, Class<T> serviceType) {
375
    if (this.beanFactory == null) {
3!
376
      throw new IllegalStateException(
×
377
              "BeanFactory must be set on cache aspect for %s retrieval".formatted(serviceType.getSimpleName()));
×
378
    }
379
    return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, serviceType, name);
6✔
380
  }
381

382
  /**
383
   * Clear the cached metadata.
384
   */
385
  protected void clearMetadataCache() {
386
    this.metadataCache.clear();
3✔
387
    this.evaluator.clear();
3✔
388
  }
1✔
389

390
  @Nullable
391
  protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
392
    // Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
393
    if (this.initialized) {
3!
394
      Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);
3✔
395
      CacheOperationSource cacheOperationSource = getCacheOperationSource();
3✔
396
      if (cacheOperationSource != null) {
2!
397
        Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
5✔
398
        if (CollectionUtils.isNotEmpty(operations)) {
3✔
399
          return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass));
14✔
400
        }
401
      }
402
    }
403

404
    return invokeOperation(invoker);
4✔
405
  }
406

407
  /**
408
   * Execute the underlying operation (typically in case of cache miss) and return
409
   * the result of the invocation. If an exception occurs it will be wrapped in a
410
   * {@link CacheOperationInvoker.ThrowableWrapper}: the exception can be handled
411
   * or modified but it <em>must</em> be wrapped in a
412
   * {@link CacheOperationInvoker.ThrowableWrapper} as well.
413
   *
414
   * @param invoker the invoker handling the operation being cached
415
   * @return the result of the invocation
416
   * @see CacheOperationInvoker#invoke()
417
   */
418
  @Nullable
419
  protected Object invokeOperation(CacheOperationInvoker invoker) {
420
    return invoker.invoke();
3✔
421
  }
422

423
  @Nullable
424
  private Object execute(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
425
    if (contexts.isSynchronized()) {
3✔
426
      // Special handling of synchronized invocation
427
      return executeSynchronized(invoker, method, contexts);
6✔
428
    }
429

430
    // Process any early evictions
431
    processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
8✔
432
            CacheOperationExpressionEvaluator.NO_RESULT);
433

434
    // Check if we have a cached value matching the conditions
435
    Object cacheHit = findCachedValue(invoker, method, contexts);
6✔
436
    if (cacheHit == null || cacheHit instanceof Cache.ValueWrapper) {
5✔
437
      return evaluate(cacheHit, invoker, method, contexts);
7✔
438
    }
439
    return cacheHit;
2✔
440
  }
441

442
  @Nullable
443
  @SuppressWarnings("NullAway")
444
  private Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
445
    CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
7✔
446
    if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
5✔
447
      Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
5✔
448
      Cache cache = context.getCaches().iterator().next();
6✔
449
      if (isAsync(method.getReturnType())) {
4✔
450
        AtomicBoolean invokeFailure = new AtomicBoolean(false);
5✔
451
        CompletableFuture<?> result = doRetrieve(cache, key,
15✔
452
                () -> {
453
                  CompletableFuture<?> invokeResult = invokeAsCompletableFuture(invoker);
4✔
454
                  if (invokeResult == null) {
2!
455
                    throw new IllegalStateException("Returned CompletableFuture must not be null: " + method);
×
456
                  }
457
                  return invokeResult.exceptionallyCompose(ex -> {
5✔
458
                    invokeFailure.set(true);
3✔
459
                    return CompletableFuture.failedFuture(ex);
3✔
460
                  });
461
                })
462
                .exceptionallyCompose(ex -> {
2✔
463
                  if (!(ex instanceof RuntimeException rex)) {
7!
464
                    return CompletableFuture.failedFuture(ex);
×
465
                  }
466
                  try {
467
                    getErrorHandler().handleCacheGetError(rex, cache, key);
6✔
468
                    if (invokeFailure.get()) {
3✔
469
                      return CompletableFuture.failedFuture(ex);
3✔
470
                    }
471
                    return invokeAsCompletableFuture(invoker);
4✔
472
                  }
473
                  catch (Throwable ex2) {
1✔
474
                    return CompletableFuture.failedFuture(ex2);
3✔
475
                  }
476
                });
477

478
        if (Future.class == context.getMethod().getReturnType()) {
5✔
479
          return Future.forAdaption(result);
3✔
480
        }
481
        return result;
2✔
482
      }
483
      if (this.reactiveCachingHandler != null) {
3!
484
        Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key);
8✔
485
        if (returnValue != ReactiveCachingHandler.NOT_HANDLED) {
3✔
486
          return returnValue;
2✔
487
        }
488
      }
489
      try {
490
        return wrapCacheValue(method, doGet(cache, key, k -> unwrapReturnValue(invokeOperation(invoker))));
15✔
491
      }
492
      catch (Cache.ValueRetrievalException ex) {
1✔
493
        // Directly propagate ThrowableWrapper from the invoker,
494
        // or potentially also an IllegalArgumentException etc.
495
        ReflectionUtils.rethrowRuntimeException(ex.getCause());
×
496
        // Never reached
497
        return null;
×
498
      }
499
    }
500
    else {
501
      // No caching required, just call the underlying method
502
      return invokeOperation(invoker);
4✔
503
    }
504
  }
505

506
  @Nullable
507
  @SuppressWarnings({ "unchecked", "rawtypes" })
508
  private <T> CompletableFuture<T> invokeAsCompletableFuture(CacheOperationInvoker invoker) {
509
    Object object = invokeOperation(invoker);
4✔
510
    if (object instanceof Future future) {
6✔
511
      return future.completable();
3✔
512
    }
513
    return (CompletableFuture<T>) object;
3✔
514
  }
515

516
  /**
517
   * Find a cached value only for {@link CacheableOperation} that passes the condition.
518
   *
519
   * @param contexts the cacheable operations
520
   * @return a {@link Cache.ValueWrapper} holding the cached value,
521
   * or {@code null} if none is found
522
   */
523
  @Nullable
524
  private Object findCachedValue(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
525
    for (CacheOperationContext context : contexts.get(CacheableOperation.class)) {
12✔
526
      if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
5✔
527
        Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
5✔
528
        Object cached = findInCaches(context, key, invoker, method, contexts);
8✔
529
        if (cached != null) {
2✔
530
          if (log.isTraceEnabled()) {
3!
531
            log.trace("Cache entry for key '{}' found in cache(s) {}", key, context.getCacheNames());
×
532
          }
533
          return cached;
2✔
534
        }
535
        else {
536
          if (log.isTraceEnabled()) {
3!
537
            log.trace("No cache entry for key '{}' in cache(s) {}", key, context.getCacheNames());
×
538
          }
539
        }
540
      }
541
    }
1✔
542
    return null;
2✔
543
  }
544

545
  @Nullable
546
  private Object findInCaches(CacheOperationContext context, Object key,
547
          CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
548

549
    boolean async = isAsync(context.getMethod().getReturnType());
5✔
550
    for (Cache cache : context.getCaches()) {
11✔
551
      if (async) {
2✔
552
        CompletableFuture<?> result = doRetrieve(cache, key);
5✔
553
        if (result != null) {
2✔
554
          result = result.exceptionallyCompose(ex -> {
11✔
555
            if (!(ex instanceof RuntimeException rex)) {
7!
556
              return CompletableFuture.failedFuture(ex);
×
557
            }
558
            try {
559
              getErrorHandler().handleCacheGetError(rex, cache, key);
6✔
560
              return CompletableFuture.completedFuture(null);
3✔
561
            }
562
            catch (Throwable ex2) {
1✔
563
              return CompletableFuture.failedFuture(ex2);
3✔
564
            }
565
          }).thenCompose(value -> (CompletableFuture<?>) evaluate(
6✔
566
                  (value != null ? CompletableFuture.completedFuture(unwrapCacheValue(value)) : null),
11✔
567
                  invoker, method, contexts));
568

569
          if (Future.class == context.getMethod().getReturnType()) {
5!
570
            return Future.forAdaption(result);
×
571
          }
572
          return result;
2✔
573
        }
574
        else {
575
          continue;
576
        }
577
      }
578
      if (reactiveCachingHandler != null) {
3!
579
        Object returnValue = reactiveCachingHandler.findInCaches(context, cache, key, invoker, method, contexts);
10✔
580
        if (returnValue != ReactiveCachingHandler.NOT_HANDLED) {
3✔
581
          return returnValue;
2✔
582
        }
583
      }
584
      Cache.ValueWrapper result = doGet(cache, key);
5✔
585
      if (result != null) {
2✔
586
        return result;
2✔
587
      }
588
    }
1✔
589
    return null;
2✔
590
  }
591

592
  private static boolean isAsync(Class<?> type) {
593
    return java.util.concurrent.Future.class == type || Future.class == type
16!
594
            || CompletionStage.class == type || CompletableFuture.class == type;
595
  }
596

597
  @Nullable
598
  private Object evaluate(@Nullable Object cacheHit, CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
599
    // Re-invocation in reactive pipeline after late cache hit determination?
600
    if (contexts.processed) {
3✔
601
      return cacheHit;
2✔
602
    }
603

604
    Object cacheValue;
605
    Object returnValue;
606

607
    if (cacheHit != null && !hasCachePut(contexts)) {
6✔
608
      // If there are no put requests, just use the cache hit
609
      cacheValue = unwrapCacheValue(cacheHit);
4✔
610
      returnValue = wrapCacheValue(method, cacheValue);
5✔
611
    }
612
    else {
613
      // Invoke the method if we don't have a cache hit
614
      returnValue = invokeOperation(invoker);
4✔
615
      cacheValue = unwrapReturnValue(returnValue);
3✔
616
    }
617

618
    // Collect puts from any @Cacheable miss, if no cached value is found
619
    ArrayList<CachePutRequest> cachePutRequests = new ArrayList<>(1);
5✔
620
    if (cacheHit == null) {
2✔
621
      collectPutRequests(contexts.get(CacheableOperation.class), cacheValue, cachePutRequests);
7✔
622
    }
623

624
    // Collect any explicit @CachePuts
625
    collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
7✔
626

627
    // Process any collected put requests, either from @CachePut or a @Cacheable miss
628
    for (CachePutRequest cachePutRequest : cachePutRequests) {
10✔
629
      Object returnOverride = cachePutRequest.apply(cacheValue);
4✔
630
      if (returnOverride != null) {
2✔
631
        returnValue = returnOverride;
2✔
632
      }
633
    }
1✔
634

635
    // Process any late evictions
636
    Object returnOverride = processCacheEvicts(
5✔
637
            contexts.get(CacheEvictOperation.class), false, returnValue);
3✔
638
    if (returnOverride != null) {
2✔
639
      returnValue = returnOverride;
2✔
640
    }
641

642
    // Mark as processed for re-invocation after late cache hit determination
643
    contexts.processed = true;
3✔
644

645
    return returnValue;
2✔
646
  }
647

648
  @Nullable
649
  private static Object wrapCacheValue(Method method, @Nullable Object cacheValue) {
650
    if (method.getReturnType() == Optional.class
7!
651
            && (cacheValue == null || cacheValue.getClass() != Optional.class)) {
3!
652
      return Optional.ofNullable(cacheValue);
3✔
653
    }
654
    return cacheValue;
2✔
655
  }
656

657
  @Nullable
658
  private static Object unwrapReturnValue(@Nullable Object returnValue) {
659
    return ObjectUtils.unwrapOptional(returnValue);
3✔
660
  }
661

662
  @Nullable
663
  private Object unwrapCacheValue(@Nullable Object cacheValue) {
664
    return (cacheValue instanceof Cache.ValueWrapper wrapper ? wrapper.get() : cacheValue);
11✔
665
  }
666

667
  private boolean hasCachePut(CacheOperationContexts contexts) {
668
    // Evaluate the conditions *without* the result object because we don't have it yet...
669
    Collection<CacheOperationContext> cachePutContexts = contexts.get(CachePutOperation.class);
4✔
670
    ArrayList<CacheOperationContext> excluded = new ArrayList<>(1);
5✔
671
    for (CacheOperationContext context : cachePutContexts) {
10✔
672
      try {
673
        if (!context.isConditionPassing(CacheOperationExpressionEvaluator.RESULT_UNAVAILABLE)) {
4!
674
          excluded.add(context);
4✔
675
        }
676
      }
677
      catch (VariableNotAvailableException ex) {
1✔
678
        // Ignoring failure due to missing result, consider the cache put has to proceed
679
      }
1✔
680
    }
1✔
681
    // Check if all puts have been excluded by condition
682
    return cachePutContexts.size() != excluded.size();
9✔
683
  }
684

685
  @Nullable
686
  private Object processCacheEvicts(Collection<CacheOperationContext> contexts,
687
          boolean beforeInvocation, @Nullable Object result) {
688

689
    if (contexts.isEmpty()) {
3✔
690
      return null;
2✔
691
    }
692
    List<CacheOperationContext> applicable = contexts.stream()
4✔
693
            .filter(context -> (context.metadata.operation instanceof CacheEvictOperation evict &&
14!
694
                    beforeInvocation == evict.isBeforeInvocation())).toList();
7✔
695
    if (applicable.isEmpty()) {
3✔
696
      return null;
2✔
697
    }
698

699
    if (result instanceof CompletableFuture<?> future) {
6✔
700
      return future.whenComplete((value, ex) -> {
6✔
701
        if (ex == null) {
2!
702
          performCacheEvicts(applicable, value);
4✔
703
        }
704
      });
1✔
705
    }
706
    if (reactiveCachingHandler != null) {
3!
707
      Object returnValue = reactiveCachingHandler.processCacheEvicts(applicable, result);
6✔
708
      if (returnValue != ReactiveCachingHandler.NOT_HANDLED) {
3✔
709
        return returnValue;
2✔
710
      }
711
    }
712
    performCacheEvicts(applicable, result);
4✔
713
    return null;
2✔
714
  }
715

716
  private void performCacheEvicts(List<CacheOperationContext> contexts, @Nullable Object result) {
717
    for (CacheOperationContext context : contexts) {
10✔
718
      if (isConditionPassing(context, result)) {
5!
719
        Object key = context.getGeneratedKey();
3✔
720
        CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation;
5✔
721
        for (Cache cache : context.getCaches()) {
11✔
722
          if (operation.isCacheWide()) {
3✔
723
            logInvalidating(context, operation, null);
4✔
724
            doClear(cache, operation.isBeforeInvocation());
6✔
725
          }
726
          else {
727
            if (key == null) {
2!
728
              key = generateKey(context, result);
5✔
729
            }
730
            logInvalidating(context, operation, key);
4✔
731
            doEvict(cache, key, operation.isBeforeInvocation());
6✔
732
          }
733
        }
1✔
734
      }
735
    }
1✔
736
  }
1✔
737

738
  private static void logInvalidating(CacheOperationContext context, CacheEvictOperation operation, @Nullable Object key) {
739
    if (log.isTraceEnabled()) {
3!
740
      log.trace("Invalidating {} for operation {} on method {}",
×
741
              (key != null ? "cache key [" + key + "]" : "entire cache"), operation, context.metadata.method);
×
742
    }
743
  }
1✔
744

745
  /**
746
   * Collect the {@link CachePutRequest} for all {@link CacheOperation} using
747
   * the specified result value.
748
   *
749
   * @param contexts the contexts to handle
750
   * @param result the result value (never {@code null})
751
   * @param putRequests the collection to update
752
   */
753
  private void collectPutRequests(Collection<CacheOperationContext> contexts,
754
          @Nullable Object result, ArrayList<CachePutRequest> putRequests) {
755

756
    for (CacheOperationContext context : contexts) {
10✔
757
      if (isConditionPassing(context, result)) {
5✔
758
        putRequests.add(new CachePutRequest(context));
8✔
759
      }
760
    }
1✔
761
  }
1✔
762

763
  private boolean isConditionPassing(CacheOperationContext context, @Nullable Object result) {
764
    boolean passing = context.isConditionPassing(result);
4✔
765
    if (!passing && log.isTraceEnabled()) {
5!
766
      log.trace("Cache condition failed on method {} for operation {}", context.metadata.method, context.metadata.operation);
×
767
    }
768
    return passing;
2✔
769
  }
770

771
  private Object generateKey(CacheOperationContext context, @Nullable Object result) {
772
    Object key = context.generateKey(result);
4✔
773
    if (key == null) {
2!
774
      throw new IllegalArgumentException("""
×
775
              Null key returned for cache operation [%s]. If you are using named parameters, \
776
              ensure that the compiler uses the '-parameters' flag."""
777
              .formatted(context.metadata.operation));
×
778
    }
779
    if (log.isTraceEnabled()) {
3!
780
      log.trace("Computed cache key '{}' for operation {]", key, context.metadata.operation);
×
781
    }
782
    return key;
2✔
783
  }
784

785
  private class CacheOperationContexts {
786

787
    private final MultiValueMap<Class<? extends CacheOperation>, CacheOperationContext> contexts;
788

789
    private final boolean sync;
790

791
    public boolean processed;
792

793
    public CacheOperationContexts(Collection<? extends CacheOperation> operations,
794
            Method method, Object[] args, Object target, Class<?> targetClass) {
5✔
795

796
      this.contexts = MultiValueMap.forLinkedHashMap(operations.size());
5✔
797
      for (CacheOperation op : operations) {
10✔
798
        this.contexts.add(op.getClass(), getOperationContext(op, method, args, target, targetClass));
12✔
799
      }
1✔
800
      this.sync = determineSyncFlag(method);
5✔
801
    }
1✔
802

803
    public Collection<CacheOperationContext> get(Class<? extends CacheOperation> operationClass) {
804
      Collection<CacheOperationContext> result = this.contexts.get(operationClass);
6✔
805
      return (result != null ? result : Collections.emptyList());
6✔
806
    }
807

808
    public boolean isSynchronized() {
809
      return this.sync;
3✔
810
    }
811

812
    private boolean determineSyncFlag(Method method) {
813
      List<CacheOperationContext> cacheableContexts = this.contexts.get(CacheableOperation.class);
6✔
814
      if (cacheableContexts == null) {  // no @Cacheable operation at all
2✔
815
        return false;
2✔
816
      }
817
      boolean syncEnabled = false;
2✔
818
      for (CacheOperationContext context : cacheableContexts) {
10✔
819
        if (context.getOperation() instanceof CacheableOperation cacheable && cacheable.isSync()) {
12!
820
          syncEnabled = true;
2✔
821
          break;
1✔
822
        }
823
      }
1✔
824
      if (syncEnabled) {
2✔
825
        if (this.contexts.size() > 1) {
5✔
826
          throw new IllegalStateException(
7✔
827
                  "A sync=true operation cannot be combined with other cache operations on '" + method + "'");
828
        }
829
        if (cacheableContexts.size() > 1) {
4✔
830
          throw new IllegalStateException(
7✔
831
                  "Only one sync=true operation is allowed on '" + method + "'");
832
        }
833
        CacheOperationContext cacheableContext = cacheableContexts.iterator().next();
5✔
834
        CacheOperation operation = cacheableContext.getOperation();
3✔
835
        if (cacheableContext.getCaches().size() > 1) {
5✔
836
          throw new IllegalStateException(
7✔
837
                  "A sync=true operation is restricted to a single cache on '" + operation + "'");
838
        }
839
        if (operation instanceof CacheableOperation cacheable && StringUtils.hasText(cacheable.getUnless())) {
10!
840
          throw new IllegalStateException(
7✔
841
                  "A sync=true operation does not support the unless attribute on '" + operation + "'");
842
        }
843
        return true;
2✔
844
      }
845
      return false;
2✔
846
    }
847
  }
848

849
  /**
850
   * Metadata of a cache operation that does not depend on a particular invocation
851
   * which makes it a good candidate for caching.
852
   */
853
  protected static class CacheOperationMetadata {
854
    public final CacheOperation operation;
855

856
    public final Method method;
857

858
    public final Class<?> targetClass;
859

860
    public final Method targetMethod;
861

862
    public final AnnotatedElementKey methodKey;
863

864
    public final KeyGenerator keyGenerator;
865

866
    public final CacheResolver cacheResolver;
867

868
    public CacheOperationMetadata(CacheOperation operation, Method method,
869
            Class<?> targetClass, KeyGenerator keyGenerator, CacheResolver cacheResolver) {
2✔
870

871
      this.operation = operation;
3✔
872
      this.targetClass = targetClass;
3✔
873
      this.method = BridgeMethodResolver.findBridgedMethod(method);
4✔
874
      this.targetMethod = !Proxy.isProxyClass(targetClass)
4!
875
              ? AopUtils.getMostSpecificMethod(method, targetClass)
4✔
876
              : this.method;
1✔
877
      this.methodKey = new AnnotatedElementKey(this.targetMethod, targetClass);
8✔
878
      this.keyGenerator = keyGenerator;
3✔
879
      this.cacheResolver = cacheResolver;
3✔
880
    }
1✔
881
  }
882

883
  /**
884
   * A {@link CacheOperationInvocationContext} context for a {@link CacheOperation}.
885
   */
886
  protected class CacheOperationContext implements CacheOperationInvocationContext<CacheOperation> {
887

888
    public final CacheOperationMetadata metadata;
889

890
    public final Object[] args;
891

892
    public final Object target;
893

894
    public final Collection<? extends Cache> caches;
895

896
    public final Collection<String> cacheNames;
897

898
    @Nullable
899
    public Boolean conditionPassing;
900

901
    @Nullable
902
    private Object key;
903

904
    public CacheOperationContext(CacheOperationMetadata metadata, Object[] args, Object target) {
5✔
905
      this.metadata = metadata;
3✔
906
      this.args = extractArgs(metadata.method, args);
7✔
907
      this.target = target;
3✔
908
      this.caches = CacheAspectSupport.this.getCaches(this, metadata.cacheResolver);
7✔
909
      this.cacheNames = createCacheNames(this.caches);
6✔
910
    }
1✔
911

912
    @Override
913
    public CacheOperation getOperation() {
914
      return this.metadata.operation;
4✔
915
    }
916

917
    @Override
918
    public Object getTarget() {
919
      return this.target;
×
920
    }
921

922
    @Override
923
    public Method getMethod() {
924
      return this.metadata.method;
4✔
925
    }
926

927
    @Override
928
    public Object[] getArgs() {
929
      return this.args;
3✔
930
    }
931

932
    private Object[] extractArgs(Method method, Object[] args) {
933
      if (!method.isVarArgs()) {
3✔
934
        return args;
2✔
935
      }
936
      Object[] varArgs = ObjectUtils.toObjectArray(args[args.length - 1]);
8✔
937
      Object[] combinedArgs = new Object[args.length - 1 + varArgs.length];
9✔
938
      System.arraycopy(args, 0, combinedArgs, 0, args.length - 1);
9✔
939
      System.arraycopy(varArgs, 0, combinedArgs, args.length - 1, varArgs.length);
10✔
940
      return combinedArgs;
2✔
941
    }
942

943
    protected boolean isConditionPassing(@Nullable Object result) {
944
      if (this.conditionPassing == null) {
3✔
945
        if (metadata.operation.hasConditionString()) {
5✔
946
          EvaluationContext evaluationContext = createEvaluationContext(result);
4✔
947
          this.conditionPassing = evaluator.condition(metadata.operation.getCondition(),
15✔
948
                  metadata.methodKey, evaluationContext);
949
        }
1✔
950
        else {
951
          this.conditionPassing = true;
4✔
952
        }
953
      }
954
      return this.conditionPassing;
4✔
955
    }
956

957
    protected boolean canPutToCache(@Nullable Object value) {
958
      String unless;
959
      if (metadata.operation instanceof CacheableOperation cacheable) {
10✔
960
        unless = cacheable.getUnless();
4✔
961
      }
962
      else if (metadata.operation instanceof CachePutOperation cachePut) {
10!
963
        unless = cachePut.getUnless();
4✔
964
      }
965
      else {
966
        return true;
×
967
      }
968
      if (StringUtils.hasText(unless)) {
3✔
969
        EvaluationContext evaluationContext = createEvaluationContext(value);
4✔
970
        return !evaluator.unless(unless, metadata.methodKey, evaluationContext);
14✔
971
      }
972
      return true;
2✔
973
    }
974

975
    /**
976
     * Compute the key for the given caching operation.
977
     */
978
    @Nullable
979
    protected Object generateKey(@Nullable Object result) {
980
      if (metadata.operation.hasKeyString()) {
5✔
981
        EvaluationContext evaluationContext = createEvaluationContext(result);
4✔
982
        this.key = evaluator.key(metadata.operation.getKey(), metadata.methodKey, evaluationContext);
14✔
983
      }
1✔
984
      else {
985
        this.key = metadata.keyGenerator.generate(this.target, this.metadata.method, this.args);
13✔
986
      }
987
      return this.key;
3✔
988
    }
989

990
    @Nullable
991
    protected Object getGeneratedKey() {
992
      return this.key;
3✔
993
    }
994

995
    private EvaluationContext createEvaluationContext(@Nullable Object result) {
996
      return evaluator.createEvaluationContext(this.caches, this.metadata.method, this.args,
21✔
997
              this.target, this.metadata.targetClass, this.metadata.targetMethod, result);
998
    }
999

1000
    protected Collection<? extends Cache> getCaches() {
1001
      return this.caches;
3✔
1002
    }
1003

1004
    protected Collection<String> getCacheNames() {
1005
      return this.cacheNames;
×
1006
    }
1007

1008
    private Collection<String> createCacheNames(Collection<? extends Cache> caches) {
1009
      ArrayList<String> names = new ArrayList<>(caches.size());
6✔
1010
      for (Cache cache : caches) {
10✔
1011
        names.add(cache.getName());
5✔
1012
      }
1✔
1013
      return names;
2✔
1014
    }
1015
  }
1016

1017
  private static final class CacheOperationCacheKey implements Comparable<CacheOperationCacheKey> {
1018

1019
    private final CacheOperation cacheOperation;
1020

1021
    private final AnnotatedElementKey methodCacheKey;
1022

1023
    private CacheOperationCacheKey(CacheOperation cacheOperation, Method method, Class<?> targetClass) {
2✔
1024
      this.cacheOperation = cacheOperation;
3✔
1025
      this.methodCacheKey = new AnnotatedElementKey(method, targetClass);
7✔
1026
    }
1✔
1027

1028
    @Override
1029
    public boolean equals(@Nullable Object other) {
1030
      return this == other
1✔
1031
              || (other instanceof CacheOperationCacheKey that
13!
1032
              && this.cacheOperation.equals(that.cacheOperation)
6!
1033
              && this.methodCacheKey.equals(that.methodCacheKey));
5✔
1034
    }
1035

1036
    @Override
1037
    public int hashCode() {
1038
      return (this.cacheOperation.hashCode() * 31 + this.methodCacheKey.hashCode());
10✔
1039
    }
1040

1041
    @Override
1042
    public String toString() {
1043
      return this.cacheOperation + " on " + this.methodCacheKey;
×
1044
    }
1045

1046
    @Override
1047
    public int compareTo(CacheOperationCacheKey other) {
1048
      int result = this.cacheOperation.getName().compareTo(other.cacheOperation.getName());
×
1049
      if (result == 0) {
×
1050
        result = this.methodCacheKey.compareTo(other.methodCacheKey);
×
1051
      }
1052
      return result;
×
1053
    }
1054
  }
1055

1056
  private class CachePutRequest {
1057

1058
    private final CacheOperationContext context;
1059

1060
    public CachePutRequest(CacheOperationContext context) {
5✔
1061
      this.context = context;
3✔
1062
    }
1✔
1063

1064
    @Nullable
1065
    public Object apply(@Nullable Object result) {
1066
      if (result instanceof CompletableFuture<?> future) {
6✔
1067
        return future.whenComplete((value, ex) -> {
5✔
1068
          if (ex == null) {
2✔
1069
            performCachePut(value);
3✔
1070
          }
1071
        });
1✔
1072
      }
1073
      if (reactiveCachingHandler != null) {
4!
1074
        Object returnValue = reactiveCachingHandler.processPutRequest(this, result);
7✔
1075
        if (returnValue != ReactiveCachingHandler.NOT_HANDLED) {
3✔
1076
          return returnValue;
2✔
1077
        }
1078
      }
1079
      performCachePut(result);
3✔
1080
      return null;
2✔
1081
    }
1082

1083
    public void performCachePut(@Nullable Object value) {
1084
      if (this.context.canPutToCache(value)) {
5✔
1085
        Object key = this.context.getGeneratedKey();
4✔
1086
        if (key == null) {
2✔
1087
          key = generateKey(this.context, value);
7✔
1088
        }
1089
        if (log.isTraceEnabled()) {
3!
1090
          log.trace("Creating cache entry for key '{}' in cache(s) {}", key, this.context.getCacheNames());
×
1091
        }
1092
        for (Cache cache : this.context.getCaches()) {
12✔
1093
          doPut(cache, key, value);
6✔
1094
        }
1✔
1095
      }
1096
    }
1✔
1097
  }
1098

1099
  /**
1100
   * Reactive Streams Subscriber collection for collecting a List to cache.
1101
   */
1102
  private static class CachePutListSubscriber implements Subscriber<Object> {
1103

1104
    private final CachePutRequest request;
1105

1106
    private final ArrayList<Object> cacheValue = new ArrayList<>();
5✔
1107

1108
    public CachePutListSubscriber(CachePutRequest request) {
2✔
1109
      this.request = request;
3✔
1110
    }
1✔
1111

1112
    @Override
1113
    public void onSubscribe(Subscription s) {
1114
      s.request(Integer.MAX_VALUE);
3✔
1115
    }
1✔
1116

1117
    @Override
1118
    public void onNext(Object o) {
1119
      this.cacheValue.add(o);
5✔
1120
    }
1✔
1121

1122
    @Override
1123
    public void onError(Throwable t) {
1124
    }
1✔
1125

1126
    @Override
1127
    public void onComplete() {
1128
      this.request.performCachePut(this.cacheValue);
5✔
1129
    }
1✔
1130
  }
1131

1132
  /**
1133
   * Inner class to avoid a hard dependency on the Reactive Streams API at runtime.
1134
   */
1135
  private final class ReactiveCachingHandler {
5✔
1136

1137
    public static final Object NOT_HANDLED = new Object();
5✔
1138

1139
    private final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance();
4✔
1140

1141
    public Object executeSynchronized(CacheOperationInvoker invoker, Method method, Cache cache, Object key) {
1142
      AtomicBoolean invokeFailure = new AtomicBoolean(false);
5✔
1143
      ReactiveAdapter adapter = registry.getAdapter(method.getReturnType());
6✔
1144
      if (adapter != null) {
2✔
1145
        if (adapter.isMultiValue()) {
3✔
1146
          // Flux or similar
1147
          return adapter.fromPublisher(Flux.from(Mono.fromFuture(
15✔
1148
                          doRetrieve(cache, key, () -> Flux.from(adapter.toPublisher(invokeOperation(invoker)))
9✔
1149
                                  .collectList().doOnError(ex -> invokeFailure.set(true)).toFuture())))
9✔
1150
                  .flatMap(Flux::fromIterable)
9✔
1151
                  .onErrorResume(RuntimeException.class, ex -> {
1✔
1152
                    try {
1153
                      getErrorHandler().handleCacheGetError(ex, cache, key);
7✔
1154
                      if (invokeFailure.get()) {
3✔
1155
                        return Flux.error(ex);
3✔
1156
                      }
1157
                      return Flux.from(adapter.toPublisher(invokeOperation(invoker)));
8✔
1158
                    }
1159
                    catch (RuntimeException exception) {
1✔
1160
                      return Flux.error(exception);
3✔
1161
                    }
1162
                  }));
1163
        }
1164
        else {
1165
          // Mono or similar
1166
          return adapter.fromPublisher(Mono.fromFuture(doRetrieve(cache, key,
22✔
1167
                          () -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).doOnError(ex -> invokeFailure.set(true)).toFuture()))
16✔
1168
                  .onErrorResume(RuntimeException.class, ex -> {
1✔
1169
                    try {
1170
                      getErrorHandler().handleCacheGetError(ex, cache, key);
7✔
1171
                      if (invokeFailure.get()) {
3✔
1172
                        return Mono.error(ex);
3✔
1173
                      }
1174
                      return Mono.from(adapter.toPublisher(invokeOperation(invoker)));
8✔
1175
                    }
1176
                    catch (RuntimeException exception) {
1✔
1177
                      return Mono.error(exception);
3✔
1178
                    }
1179
                  }));
1180
        }
1181
      }
1182
      return NOT_HANDLED;
2✔
1183
    }
1184

1185
    public Object processCacheEvicts(List<CacheOperationContext> contexts, @Nullable Object result) {
1186
      ReactiveAdapter adapter = (result != null ? registry.getAdapter(result.getClass()) : null);
10✔
1187
      if (adapter != null) {
2✔
1188
        return adapter.fromPublisher(Mono.from(adapter.toPublisher(result))
10✔
1189
                .doOnSuccess(value -> performCacheEvicts(contexts, value)));
7✔
1190
      }
1191
      return NOT_HANDLED;
2✔
1192
    }
1193

1194
    @Nullable
1195
    @SuppressWarnings({ "rawtypes", "unchecked" })
1196
    public Object findInCaches(CacheOperationContext context, Cache cache, Object key,
1197
            CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
1198

1199
      ReactiveAdapter adapter = registry.getAdapter(context.getMethod().getReturnType());
7✔
1200
      if (adapter != null) {
2✔
1201
        CompletableFuture<?> cachedFuture = doRetrieve(cache, key);
6✔
1202
        if (cachedFuture == null) {
2✔
1203
          return null;
2✔
1204
        }
1205
        if (adapter.isMultiValue()) {
3✔
1206
          return adapter.fromPublisher(Flux.from(Mono.fromFuture(cachedFuture))
11✔
1207
                  .switchIfEmpty(Flux.defer(() -> (Flux) evaluate(null, invoker, method, contexts)))
16✔
1208
                  .flatMap(v -> evaluate(valueToFlux(v, contexts), invoker, method, contexts))
20✔
1209
                  .onErrorResume(RuntimeException.class, ex -> {
1✔
1210
                    try {
1211
                      getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key);
8✔
1212
                      Object e = evaluate(null, invoker, method, contexts);
8✔
1213
                      return (e != null ? e : Flux.error((RuntimeException) ex));
5!
1214
                    }
1215
                    catch (RuntimeException exception) {
1✔
1216
                      return Flux.error(exception);
3✔
1217
                    }
1218
                  }));
1219
        }
1220
        else {
1221
          return adapter.fromPublisher(Mono.fromFuture(cachedFuture)
10✔
1222
                  .switchIfEmpty(Mono.defer(() -> (Mono) evaluate(null, invoker, method, contexts)))
16✔
1223
                  .flatMap(v -> evaluate(Mono.justOrEmpty(unwrapCacheValue(v)), invoker, method, contexts))
21✔
1224
                  .onErrorResume(RuntimeException.class, ex -> {
1✔
1225
                    try {
1226
                      getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key);
8✔
1227
                      Object e = evaluate(null, invoker, method, contexts);
8✔
1228
                      return (e != null ? e : Mono.error((RuntimeException) ex));
5!
1229
                    }
1230
                    catch (RuntimeException exception) {
1✔
1231
                      return Mono.error(exception);
3✔
1232
                    }
1233
                  }));
1234
        }
1235
      }
1236
      return NOT_HANDLED;
2✔
1237
    }
1238

1239
    private Flux<?> valueToFlux(Object value, CacheOperationContexts contexts) {
1240
      Object data = unwrapCacheValue(value);
5✔
1241
      return (!contexts.processed && data instanceof Iterable<?> iterable ? Flux.fromIterable(iterable) :
13✔
1242
              (data != null ? Flux.just(data) : Flux.empty()));
5!
1243
    }
1244

1245
    public Object processPutRequest(CachePutRequest request, @Nullable Object result) {
1246
      ReactiveAdapter adapter = (result != null ? registry.getAdapter(result.getClass()) : null);
10✔
1247
      if (adapter != null) {
2✔
1248
        if (adapter.isMultiValue()) {
3✔
1249
          Flux<?> source = Flux.from(adapter.toPublisher(result))
4✔
1250
                  .publish().refCount(2);
4✔
1251
          source.subscribe(new CachePutListSubscriber(request));
6✔
1252
          return adapter.fromPublisher(source);
4✔
1253
        }
1254
        else {
1255
          return adapter.fromPublisher(Mono.from(adapter.toPublisher(result))
9✔
1256
                  .doOnSuccess(request::performCachePut));
4✔
1257
        }
1258
      }
1259
      return NOT_HANDLED;
2✔
1260
    }
1261
  }
1262

1263
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc