Support for late-determined cache misses from retrieve(key)
Closes gh-31637
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user