Support for late-determined cache misses from retrieve(key)

Closes gh-31637
This commit is contained in:
Juergen Hoeller
2023-11-21 23:11:34 +01:00
parent 8ffbecc7c3
commit 1410c466b7
6 changed files with 413 additions and 121 deletions

View File

@@ -25,9 +25,9 @@ import org.springframework.lang.Nullable;
/**
* Interface that defines common cache operations.
*
* <p>Serves as an SPI for Spring's annotation-based caching model
* ({@link org.springframework.cache.annotation.Cacheable} and co)
* as well as an API for direct usage in applications.
* <p>Serves primarily as an SPI for Spring's annotation-based caching
* model ({@link org.springframework.cache.annotation.Cacheable} and co)
* and secondarily as an API for direct usage in applications.
*
* <p><b>Note:</b> Due to the generic use of caching, it is recommended
* that implementations allow storage of {@code null} values
@@ -113,16 +113,26 @@ public interface Cache {
* wrapped in a {@link CompletableFuture}. This operation must not block
* but is allowed to return a completed {@link CompletableFuture} if the
* corresponding value is immediately available.
* <p>Returns {@code null} if the cache contains no mapping for this key;
* otherwise, the cached value (which may be {@code null} itself) will
* be returned in the {@link CompletableFuture}.
* <p>Can return {@code null} if the cache can immediately determine that
* it contains no mapping for this key (e.g. through an in-memory key map).
* Otherwise, the cached value will be returned in the {@link CompletableFuture},
* with {@code null} indicating a late-determined cache miss (and a nested
* {@link ValueWrapper} potentially indicating a nullable cached value).
* @param key the key whose associated value is to be returned
* @return the value to which this cache maps the specified key,
* contained within a {@link CompletableFuture} which may also hold
* a cached {@code null} value. A straight {@code null} being
* returned means that the cache contains no mapping for this key.
* @return the value to which this cache maps the specified key, contained
* within a {@link CompletableFuture} which may also be empty when a cache
* miss has been late-determined. A straight {@code null} being returned
* means that the cache immediately determined that it contains no mapping
* for this key. A {@link ValueWrapper} contained within the
* {@code CompletableFuture} can indicate a cached value that is potentially
* {@code null}; this is sensible in a late-determined scenario where a regular
* CompletableFuture-contained {@code null} indicates a cache miss. However,
* an early-determined cache will usually return the plain cached value here,
* and a late-determined cache may also return a plain value if it does not
* support the actual caching of {@code null} values. Spring's common cache
* processing can deal with all variants of these implementation strategies.
* @since 6.1
* @see #get(Object)
* @see #retrieve(Object, Supplier)
*/
@Nullable
default CompletableFuture<?> retrieve(Object key) {

View File

@@ -363,7 +363,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
if (this.initialized) {
Class<?> targetClass = getTargetClass(target);
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(target);
CacheOperationSource cacheOperationSource = getCacheOperationSource();
if (cacheOperationSource != null) {
Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
@@ -374,7 +374,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
}
}
return invoker.invoke();
return invokeOperation(invoker);
}
/**
@@ -392,10 +392,6 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
return invoker.invoke();
}
private Class<?> getTargetClass(Object target) {
return AopProxyUtils.ultimateTargetClass(target);
}
@Nullable
private Object execute(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
if (contexts.isSynchronized()) {
@@ -408,7 +404,104 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
CacheOperationExpressionEvaluator.NO_RESULT);
// Check if we have a cached value matching the conditions
Object cacheHit = findCachedValue(contexts.get(CacheableOperation.class));
Object cacheHit = findCachedValue(invoker, method, contexts);
if (cacheHit == null || cacheHit instanceof Cache.ValueWrapper) {
return evaluate(cacheHit, invoker, method, contexts);
}
return cacheHit;
}
@Nullable
private Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) {
return cache.retrieve(key, () -> (CompletableFuture<?>) invokeOperation(invoker));
}
if (this.reactiveCachingHandler != null) {
Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key);
if (returnValue != ReactiveCachingHandler.NOT_HANDLED) {
return returnValue;
}
}
try {
return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
}
catch (Cache.ValueRetrievalException ex) {
// Directly propagate ThrowableWrapper from the invoker,
// or potentially also an IllegalArgumentException etc.
ReflectionUtils.rethrowRuntimeException(ex.getCause());
// Never reached
return null;
}
}
else {
// No caching required, just call the underlying method
return invokeOperation(invoker);
}
}
/**
* Find a cached value only for {@link CacheableOperation} that passes the condition.
* @param contexts the cacheable operations
* @return a {@link Cache.ValueWrapper} holding the cached value,
* or {@code null} if none is found
*/
@Nullable
private Object findCachedValue(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
for (CacheOperationContext context : contexts.get(CacheableOperation.class)) {
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Object cached = findInCaches(context, key, invoker, method, contexts);
if (cached != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache(s) " + context.getCacheNames());
}
return cached;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
}
}
}
}
return null;
}
@Nullable
private Object findInCaches(CacheOperationContext context, Object key,
CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
for (Cache cache : context.getCaches()) {
if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) {
CompletableFuture<?> result = cache.retrieve(key);
if (result != null) {
return result.thenCompose(value -> (CompletableFuture<?>) evaluate(
(value != null ? CompletableFuture.completedFuture(unwrapCacheValue(value)) : null),
invoker, method, contexts));
}
}
if (this.reactiveCachingHandler != null) {
Object returnValue = this.reactiveCachingHandler.findInCaches(
context, cache, key, invoker, method, contexts);
if (returnValue != ReactiveCachingHandler.NOT_HANDLED) {
return returnValue;
}
}
Cache.ValueWrapper result = doGet(cache, key);
if (result != null) {
return result;
}
}
return null;
}
@Nullable
private Object evaluate(@Nullable Object cacheHit, CacheOperationInvoker invoker, Method method,
CacheOperationContexts contexts) {
Object cacheValue;
Object returnValue;
@@ -452,35 +545,8 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
}
@Nullable
private Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) {
return cache.retrieve(key, () -> (CompletableFuture<?>) invokeOperation(invoker));
}
if (this.reactiveCachingHandler != null) {
Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key);
if (returnValue != ReactiveCachingHandler.NOT_HANDLED) {
return returnValue;
}
}
try {
return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
}
catch (Cache.ValueRetrievalException ex) {
// Directly propagate ThrowableWrapper from the invoker,
// or potentially also an IllegalArgumentException etc.
ReflectionUtils.rethrowRuntimeException(ex.getCause());
// Never reached
return null;
}
}
else {
// No caching required, just call the underlying method
return invokeOperation(invoker);
}
private Object unwrapCacheValue(@Nullable Object cacheValue) {
return (cacheValue instanceof Cache.ValueWrapper wrapper ? wrapper.get() : cacheValue);
}
@Nullable
@@ -575,34 +641,6 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
}
}
/**
* Find a cached value only for {@link CacheableOperation} that passes the condition.
* @param contexts the cacheable operations
* @return a {@link Cache.ValueWrapper} holding the cached value,
* or {@code null} if none is found
*/
@Nullable
private Object findCachedValue(Collection<CacheOperationContext> contexts) {
for (CacheOperationContext context : contexts) {
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Object cached = findInCaches(context, key);
if (cached != null) {
if (logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache(s) " + context.getCacheNames());
}
return cached;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
}
}
}
}
return null;
}
/**
* Collect the {@link CachePutRequest} for all {@link CacheOperation} using
* the specified result value.
@@ -621,23 +659,6 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
}
}
@Nullable
private Object findInCaches(CacheOperationContext context, Object key) {
for (Cache cache : context.getCaches()) {
if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) {
return cache.retrieve(key);
}
if (this.reactiveCachingHandler != null) {
Object returnValue = this.reactiveCachingHandler.findInCaches(context, cache, key);
if (returnValue != ReactiveCachingHandler.NOT_HANDLED) {
return returnValue;
}
}
return doGet(cache, key);
}
return null;
}
private boolean isConditionPassing(CacheOperationContext context, @Nullable Object result) {
boolean passing = context.isConditionPassing(result);
if (!passing && logger.isTraceEnabled()) {
@@ -1048,8 +1069,11 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
return NOT_HANDLED;
}
@SuppressWarnings("unchecked")
@Nullable
public Object findInCaches(CacheOperationContext context, Cache cache, Object key) {
public Object findInCaches(CacheOperationContext context, Cache cache, Object key,
CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
ReactiveAdapter adapter = this.registry.getAdapter(context.getMethod().getReturnType());
if (adapter != null) {
CompletableFuture<?> cachedFuture = cache.retrieve(key);
@@ -1057,11 +1081,16 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
return null;
}
if (adapter.isMultiValue()) {
return adapter.fromPublisher(Flux.from(Mono.fromFuture(cachedFuture))
.flatMap(v -> (v instanceof Iterable<?> iv ? Flux.fromIterable(iv) : Flux.just(v))));
return adapter.fromPublisher(Flux.from(
Mono.fromFuture(cachedFuture)
.flatMap(value -> (Mono<?>) evaluate(Mono.just(unwrapCacheValue(value)), invoker, method, contexts)))
.flatMap(v -> (v instanceof Iterable<?> iv ? Flux.fromIterable(iv) : Flux.just(v)))
.switchIfEmpty(Flux.defer(() -> (Flux<?>) evaluate(null, invoker, method, contexts))));
}
else {
return adapter.fromPublisher(Mono.fromFuture(cachedFuture));
return adapter.fromPublisher(Mono.fromFuture(cachedFuture)
.flatMap(value -> (Mono<?>) evaluate(Mono.just(unwrapCacheValue(value)), invoker, method, contexts))
.switchIfEmpty(Mono.defer(() -> (Mono) evaluate(null, invoker, method, contexts))));
}
}
return NOT_HANDLED;