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:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
148
spring-context/src/test/kotlin/org/springframework/cache/KotlinCacheReproTests.kt
vendored
Normal file
148
spring-context/src/test/kotlin/org/springframework/cache/KotlinCacheReproTests.kt
vendored
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user