Skip to content

Commit 466c8d8

Browse files
committed
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
1 parent 39a282e commit 466c8d8

File tree

9 files changed

+270
-9
lines changed

9 files changed

+270
-9
lines changed

Diff for: spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,11 @@ static Object asFlow(Object publisher) {
3636
return ReactiveFlowKt.asFlow((Publisher<?>) publisher);
3737
}
3838

39-
@SuppressWarnings("unchecked")
4039
@Nullable
41-
static Object awaitSingleOrNull(Object mono, Object continuation) {
42-
return MonoKt.awaitSingleOrNull((Mono<?>) mono, (Continuation<Object>) continuation);
40+
@SuppressWarnings({"unchecked", "rawtypes"})
41+
static Object awaitSingleOrNull(Object value, Object continuation) {
42+
return MonoKt.awaitSingleOrNull(value instanceof Mono mono ? mono : Mono.just(value),
43+
(Continuation<Object>) continuation);
4344
}
4445

4546
}

Diff for: spring-context/spring-context.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependencies {
3030
optional("org.hibernate:hibernate-validator")
3131
optional("org.jetbrains.kotlin:kotlin-reflect")
3232
optional("org.jetbrains.kotlin:kotlin-stdlib")
33+
optional("org.jetbrains.kotlinx:kotlinx-coroutines-core")
3334
optional("org.reactivestreams:reactive-streams")
3435
testFixturesApi("org.junit.jupiter:junit-jupiter-api")
3536
testFixturesImplementation(testFixtures(project(":spring-beans")))

Diff for: spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java

+5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.springframework.cache.CacheManager;
4949
import org.springframework.context.expression.AnnotatedElementKey;
5050
import org.springframework.core.BridgeMethodResolver;
51+
import org.springframework.core.KotlinDetector;
5152
import org.springframework.core.ReactiveAdapter;
5253
import org.springframework.core.ReactiveAdapterRegistry;
5354
import org.springframework.expression.EvaluationContext;
@@ -85,6 +86,7 @@
8586
* @author Phillip Webb
8687
* @author Sam Brannen
8788
* @author Stephane Nicoll
89+
* @author Sebastien Deleuze
8890
* @since 3.1
8991
*/
9092
public abstract class CacheAspectSupport extends AbstractCacheInvoker
@@ -1024,6 +1026,9 @@ public Object executeSynchronized(CacheOperationInvoker invoker, Method method,
10241026
() -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).toFuture())));
10251027
}
10261028
}
1029+
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isSuspendingFunction(method)) {
1030+
return Mono.fromFuture(cache.retrieve(key, () -> ((Mono<?>) invokeOperation(invoker)).toFuture()));
1031+
}
10271032
return NOT_HANDLED;
10281033
}
10291034

Diff for: spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,9 +19,15 @@
1919
import java.io.Serializable;
2020
import java.lang.reflect.Method;
2121

22+
import kotlin.coroutines.Continuation;
23+
import kotlin.coroutines.CoroutineContext;
24+
import kotlinx.coroutines.Job;
2225
import org.aopalliance.intercept.MethodInterceptor;
2326
import org.aopalliance.intercept.MethodInvocation;
27+
import org.reactivestreams.Publisher;
2428

29+
import org.springframework.core.CoroutinesUtils;
30+
import org.springframework.core.KotlinDetector;
2531
import org.springframework.lang.Nullable;
2632
import org.springframework.util.Assert;
2733

@@ -39,6 +45,7 @@
3945
*
4046
* @author Costin Leau
4147
* @author Juergen Hoeller
48+
* @author Sebastien Deleuze
4249
* @since 3.1
4350
*/
4451
@SuppressWarnings("serial")
@@ -51,6 +58,9 @@ public Object invoke(final MethodInvocation invocation) throws Throwable {
5158

5259
CacheOperationInvoker aopAllianceInvoker = () -> {
5360
try {
61+
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isSuspendingFunction(method)) {
62+
return KotlinDelegate.invokeSuspendingFunction(method, invocation.getThis(), invocation.getArguments());
63+
}
5464
return invocation.proceed();
5565
}
5666
catch (Throwable ex) {
@@ -68,4 +78,16 @@ public Object invoke(final MethodInvocation invocation) throws Throwable {
6878
}
6979
}
7080

81+
/**
82+
* Inner class to avoid a hard dependency on Kotlin at runtime.
83+
*/
84+
private static class KotlinDelegate {
85+
86+
public static Publisher<?> invokeSuspendingFunction(Method method, Object target, Object... args) {
87+
Continuation<?> continuation = (Continuation<?>) args[args.length - 1];
88+
CoroutineContext coroutineContext = continuation.getContext().minusKey(Job.Key);
89+
return CoroutinesUtils.invokeSuspendingFunction(coroutineContext, method, target, args);
90+
}
91+
}
92+
7193
}

Diff for: spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKeyGenerator.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,9 @@
1717
package org.springframework.cache.interceptor;
1818

1919
import java.lang.reflect.Method;
20+
import java.util.Arrays;
21+
22+
import org.springframework.core.KotlinDetector;
2023

2124
/**
2225
* Simple key generator. Returns the parameter itself if a single non-null
@@ -30,6 +33,7 @@
3033
*
3134
* @author Phillip Webb
3235
* @author Juergen Hoeller
36+
* @author Sebastien Deleuze
3337
* @since 4.0
3438
* @see SimpleKey
3539
* @see org.springframework.cache.annotation.CachingConfigurer
@@ -38,7 +42,8 @@ public class SimpleKeyGenerator implements KeyGenerator {
3842

3943
@Override
4044
public Object generate(Object target, Method method, Object... params) {
41-
return generateKey(params);
45+
return generateKey((KotlinDetector.isSuspendingFunction(method) ?
46+
Arrays.copyOf(params, params.length - 1) : params));
4247
}
4348

4449
/**

Diff for: spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.lang.reflect.Method;
2020
import java.util.Arrays;
2121

22+
import org.springframework.core.KotlinDetector;
2223
import org.springframework.core.ParameterNameDiscoverer;
2324
import org.springframework.expression.spel.support.StandardEvaluationContext;
2425
import org.springframework.lang.Nullable;
@@ -37,6 +38,7 @@
3738
*
3839
* @author Stephane Nicoll
3940
* @author Juergen Hoeller
41+
* @author Sebastien Deleuze
4042
* @since 4.2
4143
*/
4244
public class MethodBasedEvaluationContext extends StandardEvaluationContext {
@@ -55,7 +57,8 @@ public MethodBasedEvaluationContext(Object rootObject, Method method, Object[] a
5557

5658
super(rootObject);
5759
this.method = method;
58-
this.arguments = arguments;
60+
this.arguments = (KotlinDetector.isSuspendingFunction(method) ?
61+
Arrays.copyOf(arguments, arguments.length - 1) : arguments);
5962
this.parameterNameDiscoverer = parameterNameDiscoverer;
6063
}
6164

Diff for: spring-context/src/test/java/org/springframework/cache/interceptor/SimpleKeyGeneratorTests.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,9 +16,12 @@
1616

1717
package org.springframework.cache.interceptor;
1818

19+
import java.lang.reflect.Method;
20+
1921
import org.junit.jupiter.api.Test;
2022

2123
import org.springframework.core.testfixture.io.SerializationTestUtils;
24+
import org.springframework.util.ReflectionUtils;
2225

2326
import static org.assertj.core.api.Assertions.assertThat;
2427

@@ -28,6 +31,7 @@
2831
* @author Phillip Webb
2932
* @author Stephane Nicoll
3033
* @author Juergen Hoeller
34+
* @author Sebastien Deleuze
3135
*/
3236
public class SimpleKeyGeneratorTests {
3337

@@ -126,7 +130,8 @@ public void serializedKeys() throws Exception {
126130

127131

128132
private Object generateKey(Object[] arguments) {
129-
return this.generator.generate(null, null, arguments);
133+
Method method = ReflectionUtils.findMethod(this.getClass(), "generateKey", Object[].class);
134+
return this.generator.generate(this, method, arguments);
130135
}
131136

132137
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cache
18+
19+
import kotlinx.coroutines.runBlocking
20+
import org.assertj.core.api.Assertions.assertThat
21+
import org.junit.jupiter.api.Test
22+
import org.springframework.beans.testfixture.beans.TestBean
23+
import org.springframework.cache.CacheReproTests.*
24+
import org.springframework.cache.annotation.CacheEvict
25+
import org.springframework.cache.annotation.CachePut
26+
import org.springframework.cache.annotation.Cacheable
27+
import org.springframework.cache.annotation.EnableCaching
28+
import org.springframework.cache.concurrent.ConcurrentMapCacheManager
29+
import org.springframework.context.annotation.AnnotationConfigApplicationContext
30+
import org.springframework.context.annotation.Bean
31+
import org.springframework.context.annotation.Configuration
32+
33+
class KotlinCacheReproTests {
34+
35+
@Test
36+
fun spr14235AdaptsToSuspendingFunction() {
37+
runBlocking {
38+
val context = AnnotationConfigApplicationContext(
39+
Spr14235Config::class.java,
40+
Spr14235SuspendingService::class.java
41+
)
42+
val bean = context.getBean(Spr14235SuspendingService::class.java)
43+
val cache = context.getBean(CacheManager::class.java).getCache("itemCache")!!
44+
val tb: TestBean = bean.findById("tb1")
45+
assertThat(bean.findById("tb1")).isSameAs(tb)
46+
assertThat(cache["tb1"]!!.get()).isSameAs(tb)
47+
bean.clear()
48+
val tb2: TestBean = bean.findById("tb1")
49+
assertThat(tb2).isNotSameAs(tb)
50+
assertThat(cache["tb1"]!!.get()).isSameAs(tb2)
51+
bean.clear()
52+
bean.insertItem(tb)
53+
assertThat(bean.findById("tb1")).isSameAs(tb)
54+
assertThat(cache["tb1"]!!.get()).isSameAs(tb)
55+
context.close()
56+
}
57+
}
58+
59+
@Test
60+
fun spr14235AdaptsToSuspendingFunctionWithSync() {
61+
runBlocking {
62+
val context = AnnotationConfigApplicationContext(
63+
Spr14235Config::class.java,
64+
Spr14235SuspendingServiceSync::class.java
65+
)
66+
val bean = context.getBean(Spr14235SuspendingServiceSync::class.java)
67+
val cache = context.getBean(CacheManager::class.java).getCache("itemCache")!!
68+
val tb = bean.findById("tb1")
69+
assertThat(bean.findById("tb1")).isSameAs(tb)
70+
assertThat(cache["tb1"]!!.get()).isSameAs(tb)
71+
cache.clear()
72+
val tb2 = bean.findById("tb1")
73+
assertThat(tb2).isNotSameAs(tb)
74+
assertThat(cache["tb1"]!!.get()).isSameAs(tb2)
75+
cache.clear()
76+
bean.insertItem(tb)
77+
assertThat(bean.findById("tb1")).isSameAs(tb)
78+
assertThat(cache["tb1"]!!.get()).isSameAs(tb)
79+
context.close()
80+
}
81+
}
82+
83+
@Test
84+
fun spr15271FindsOnInterfaceWithInterfaceProxy() {
85+
val context = AnnotationConfigApplicationContext(Spr15271ConfigA::class.java)
86+
val bean = context.getBean(Spr15271Interface::class.java)
87+
val cache = context.getBean(CacheManager::class.java).getCache("itemCache")!!
88+
val tb = TestBean("tb1")
89+
bean.insertItem(tb)
90+
assertThat(bean.findById("tb1").get()).isSameAs(tb)
91+
assertThat(cache["tb1"]!!.get()).isSameAs(tb)
92+
context.close()
93+
}
94+
95+
@Test
96+
fun spr15271FindsOnInterfaceWithCglibProxy() {
97+
val context = AnnotationConfigApplicationContext(Spr15271ConfigB::class.java)
98+
val bean = context.getBean(Spr15271Interface::class.java)
99+
val cache = context.getBean(CacheManager::class.java).getCache("itemCache")!!
100+
val tb = TestBean("tb1")
101+
bean.insertItem(tb)
102+
assertThat(bean.findById("tb1").get()).isSameAs(tb)
103+
assertThat(cache["tb1"]!!.get()).isSameAs(tb)
104+
context.close()
105+
}
106+
107+
108+
open class Spr14235SuspendingService {
109+
110+
@Cacheable(value = ["itemCache"])
111+
open suspend fun findById(id: String): TestBean {
112+
return TestBean(id)
113+
}
114+
115+
@CachePut(cacheNames = ["itemCache"], key = "#item.name")
116+
open suspend fun insertItem(item: TestBean): TestBean {
117+
return item
118+
}
119+
120+
@CacheEvict(cacheNames = ["itemCache"], allEntries = true)
121+
open suspend fun clear() {
122+
}
123+
}
124+
125+
126+
open class Spr14235SuspendingServiceSync {
127+
@Cacheable(value = ["itemCache"], sync = true)
128+
open suspend fun findById(id: String): TestBean {
129+
return TestBean(id)
130+
}
131+
132+
@CachePut(cacheNames = ["itemCache"], key = "#item.name")
133+
open suspend fun insertItem(item: TestBean): TestBean {
134+
return item
135+
}
136+
}
137+
138+
139+
@Configuration(proxyBeanMethods = false)
140+
@EnableCaching
141+
class Spr14235Config {
142+
@Bean
143+
fun cacheManager(): CacheManager {
144+
return ConcurrentMapCacheManager()
145+
}
146+
}
147+
148+
}

0 commit comments

Comments
 (0)