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

@@ -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.
@@ -16,9 +16,12 @@
package org.springframework.cache.interceptor;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.springframework.core.testfixture.io.SerializationTestUtils;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -28,6 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Phillip Webb
* @author Stephane Nicoll
* @author Juergen Hoeller
* @author Sebastien Deleuze
*/
public class SimpleKeyGeneratorTests {
@@ -126,7 +130,8 @@ public class SimpleKeyGeneratorTests {
private Object generateKey(Object[] arguments) {
return this.generator.generate(null, null, arguments);
Method method = ReflectionUtils.findMethod(this.getClass(), "generateKey", Object[].class);
return this.generator.generate(this, method, arguments);
}
}

View File

@@ -0,0 +1,148 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cache
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.testfixture.beans.TestBean
import org.springframework.cache.CacheReproTests.*
import org.springframework.cache.annotation.CacheEvict
import org.springframework.cache.annotation.CachePut
import org.springframework.cache.annotation.Cacheable
import org.springframework.cache.annotation.EnableCaching
import org.springframework.cache.concurrent.ConcurrentMapCacheManager
import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
class KotlinCacheReproTests {
@Test
fun spr14235AdaptsToSuspendingFunction() {
runBlocking {
val context = AnnotationConfigApplicationContext(
Spr14235Config::class.java,
Spr14235SuspendingService::class.java
)
val bean = context.getBean(Spr14235SuspendingService::class.java)
val cache = context.getBean(CacheManager::class.java).getCache("itemCache")!!
val tb: TestBean = bean.findById("tb1")
assertThat(bean.findById("tb1")).isSameAs(tb)
assertThat(cache["tb1"]!!.get()).isSameAs(tb)
bean.clear()
val tb2: TestBean = bean.findById("tb1")
assertThat(tb2).isNotSameAs(tb)
assertThat(cache["tb1"]!!.get()).isSameAs(tb2)
bean.clear()
bean.insertItem(tb)
assertThat(bean.findById("tb1")).isSameAs(tb)
assertThat(cache["tb1"]!!.get()).isSameAs(tb)
context.close()
}
}
@Test
fun spr14235AdaptsToSuspendingFunctionWithSync() {
runBlocking {
val context = AnnotationConfigApplicationContext(
Spr14235Config::class.java,
Spr14235SuspendingServiceSync::class.java
)
val bean = context.getBean(Spr14235SuspendingServiceSync::class.java)
val cache = context.getBean(CacheManager::class.java).getCache("itemCache")!!
val tb = bean.findById("tb1")
assertThat(bean.findById("tb1")).isSameAs(tb)
assertThat(cache["tb1"]!!.get()).isSameAs(tb)
cache.clear()
val tb2 = bean.findById("tb1")
assertThat(tb2).isNotSameAs(tb)
assertThat(cache["tb1"]!!.get()).isSameAs(tb2)
cache.clear()
bean.insertItem(tb)
assertThat(bean.findById("tb1")).isSameAs(tb)
assertThat(cache["tb1"]!!.get()).isSameAs(tb)
context.close()
}
}
@Test
fun spr15271FindsOnInterfaceWithInterfaceProxy() {
val context = AnnotationConfigApplicationContext(Spr15271ConfigA::class.java)
val bean = context.getBean(Spr15271Interface::class.java)
val cache = context.getBean(CacheManager::class.java).getCache("itemCache")!!
val tb = TestBean("tb1")
bean.insertItem(tb)
assertThat(bean.findById("tb1").get()).isSameAs(tb)
assertThat(cache["tb1"]!!.get()).isSameAs(tb)
context.close()
}
@Test
fun spr15271FindsOnInterfaceWithCglibProxy() {
val context = AnnotationConfigApplicationContext(Spr15271ConfigB::class.java)
val bean = context.getBean(Spr15271Interface::class.java)
val cache = context.getBean(CacheManager::class.java).getCache("itemCache")!!
val tb = TestBean("tb1")
bean.insertItem(tb)
assertThat(bean.findById("tb1").get()).isSameAs(tb)
assertThat(cache["tb1"]!!.get()).isSameAs(tb)
context.close()
}
open class Spr14235SuspendingService {
@Cacheable(value = ["itemCache"])
open suspend fun findById(id: String): TestBean {
return TestBean(id)
}
@CachePut(cacheNames = ["itemCache"], key = "#item.name")
open suspend fun insertItem(item: TestBean): TestBean {
return item
}
@CacheEvict(cacheNames = ["itemCache"], allEntries = true)
open suspend fun clear() {
}
}
open class Spr14235SuspendingServiceSync {
@Cacheable(value = ["itemCache"], sync = true)
open suspend fun findById(id: String): TestBean {
return TestBean(id)
}
@CachePut(cacheNames = ["itemCache"], key = "#item.name")
open suspend fun insertItem(item: TestBean): TestBean {
return item
}
}
@Configuration(proxyBeanMethods = false)
@EnableCaching
class Spr14235Config {
@Bean
fun cacheManager(): CacheManager {
return ConcurrentMapCacheManager()
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cache.interceptor
import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.util.ReflectionUtils
import kotlin.coroutines.Continuation
/**
* Tests for [SimpleKeyGenerator] and [SimpleKey].
*
* @author Sebastien Deleuze
*/
class KotlinSimpleKeyGeneratorTests {
private val generator = SimpleKeyGenerator()
@Test
fun ignoreContinuationArgumentWithNoParameter() {
val method = ReflectionUtils.findMethod(javaClass, "suspendingMethod", Continuation::class.java)!!
val continuation = mockk<Continuation<Any>>()
val key = generator.generate(this, method, continuation)
assertThat(key).isEqualTo(SimpleKey.EMPTY)
}
@Test
fun ignoreContinuationArgumentWithOneParameter() {
val method = ReflectionUtils.findMethod(javaClass, "suspendingMethod", String::class.java, Continuation::class.java)!!
val continuation = mockk<Continuation<Any>>()
val key = generator.generate(this, method, "arg", continuation)
assertThat(key).isEqualTo("arg")
}
@Test
fun ignoreContinuationArgumentWithMultipleParameters() {
val method = ReflectionUtils.findMethod(javaClass, "suspendingMethod", String::class.java, String::class.java, Continuation::class.java)!!
val continuation = mockk<Continuation<Any>>()
val key = generator.generate(this, method, "arg1", "arg2", continuation)
assertThat(key).isEqualTo(SimpleKey("arg1", "arg2"))
}
@Suppress("unused", "RedundantSuspendModifier")
suspend fun suspendingMethod() {
}
@Suppress("unused", "UNUSED_PARAMETER", "RedundantSuspendModifier")
suspend fun suspendingMethod(param: String) {
}
@Suppress("unused", "UNUSED_PARAMETER", "RedundantSuspendModifier")
suspend fun suspendingMethod(param1: String, param2: String) {
}
}