Add Coroutines support for @Cacheable

This commit adds Coroutines support for `@Cacheable`.

It also refines SimpleKeyGenerator to ignore Continuation
parameters (Kotlin does not allow to have the same method
signature with both suspending and non-suspending variants)
and refines
org.springframework.aop.framework.CoroutinesUtils.awaitSingleOrNull
in order to wrap plain value to Mono.

Closes gh-31412
This commit is contained in:
Sébastien Deleuze
2023-10-15 19:11:13 +02:00
parent 39a282e463
commit 466c8d8f23
9 changed files with 270 additions and 9 deletions

View File

@@ -48,6 +48,7 @@ import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.KotlinDetector;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.expression.EvaluationContext;
@@ -85,6 +86,7 @@ import org.springframework.util.function.SupplierUtils;
* @author Phillip Webb
* @author Sam Brannen
* @author Stephane Nicoll
* @author Sebastien Deleuze
* @since 3.1
*/
public abstract class CacheAspectSupport extends AbstractCacheInvoker
@@ -1024,6 +1026,9 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
() -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).toFuture())));
}
}
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isSuspendingFunction(method)) {
return Mono.fromFuture(cache.retrieve(key, () -> ((Mono<?>) invokeOperation(invoker)).toFuture()));
}
return NOT_HANDLED;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,9 +19,15 @@ package org.springframework.cache.interceptor;
import java.io.Serializable;
import java.lang.reflect.Method;
import kotlin.coroutines.Continuation;
import kotlin.coroutines.CoroutineContext;
import kotlinx.coroutines.Job;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.reactivestreams.Publisher;
import org.springframework.core.CoroutinesUtils;
import org.springframework.core.KotlinDetector;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -39,6 +45,7 @@ import org.springframework.util.Assert;
*
* @author Costin Leau
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 3.1
*/
@SuppressWarnings("serial")
@@ -51,6 +58,9 @@ public class CacheInterceptor extends CacheAspectSupport implements MethodInterc
CacheOperationInvoker aopAllianceInvoker = () -> {
try {
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isSuspendingFunction(method)) {
return KotlinDelegate.invokeSuspendingFunction(method, invocation.getThis(), invocation.getArguments());
}
return invocation.proceed();
}
catch (Throwable ex) {
@@ -68,4 +78,16 @@ public class CacheInterceptor extends CacheAspectSupport implements MethodInterc
}
}
/**
* Inner class to avoid a hard dependency on Kotlin at runtime.
*/
private static class KotlinDelegate {
public static Publisher<?> invokeSuspendingFunction(Method method, Object target, Object... args) {
Continuation<?> continuation = (Continuation<?>) args[args.length - 1];
CoroutineContext coroutineContext = continuation.getContext().minusKey(Job.Key);
return CoroutinesUtils.invokeSuspendingFunction(coroutineContext, method, target, args);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,9 @@
package org.springframework.cache.interceptor;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.springframework.core.KotlinDetector;
/**
* Simple key generator. Returns the parameter itself if a single non-null
@@ -30,6 +33,7 @@ import java.lang.reflect.Method;
*
* @author Phillip Webb
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 4.0
* @see SimpleKey
* @see org.springframework.cache.annotation.CachingConfigurer
@@ -38,7 +42,8 @@ public class SimpleKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return generateKey(params);
return generateKey((KotlinDetector.isSuspendingFunction(method) ?
Arrays.copyOf(params, params.length - 1) : params));
}
/**

View File

@@ -19,6 +19,7 @@ package org.springframework.context.expression;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.springframework.core.KotlinDetector;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
@@ -37,6 +38,7 @@ import org.springframework.util.ObjectUtils;
*
* @author Stephane Nicoll
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 4.2
*/
public class MethodBasedEvaluationContext extends StandardEvaluationContext {
@@ -55,7 +57,8 @@ public class MethodBasedEvaluationContext extends StandardEvaluationContext {
super(rootObject);
this.method = method;
this.arguments = arguments;
this.arguments = (KotlinDetector.isSuspendingFunction(method) ?
Arrays.copyOf(arguments, arguments.length - 1) : arguments);
this.parameterNameDiscoverer = parameterNameDiscoverer;
}