Skip to content

Commit 6becfe2

Browse files
committed
Fix AOP for Kotlin suspending function with aspect + @Cacheable
This change simplifies the CacheInterceptor way of dealing with cached coroutines, thanks to the fact that lower level support for AOP has been introduced in c8169e5. This fix is similar to the one applied for `@Transactional` in gh-33095. Closes gh-33210
1 parent dfcd837 commit 6becfe2

File tree

2 files changed

+36
-23
lines changed

2 files changed

+36
-23
lines changed

Diff for: integration-tests/src/test/kotlin/org/springframework/aop/framework/autoproxy/AspectJAutoProxyInterceptorKotlinIntegrationTests.kt

+36-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ import org.junit.jupiter.api.Test
2828
import org.springframework.aop.framework.autoproxy.AspectJAutoProxyInterceptorKotlinIntegrationTests.InterceptorConfig
2929
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor
3030
import org.springframework.beans.factory.annotation.Autowired
31+
import org.springframework.cache.CacheManager
32+
import org.springframework.cache.annotation.Cacheable
33+
import org.springframework.cache.annotation.EnableCaching
34+
import org.springframework.cache.concurrent.ConcurrentMapCacheManager
3135
import org.springframework.context.annotation.Bean
3236
import org.springframework.context.annotation.Configuration
3337
import org.springframework.context.annotation.EnableAspectJAutoProxy
@@ -93,9 +97,26 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
9397
assertThat(reactiveTransactionManager.commits).`as`("transactional applied").isOne()
9498
}
9599

100+
@Test // gh-33210
101+
fun `Aspect and cacheable with suspending function`() {
102+
assertThat(countingAspect.counter).isZero()
103+
val value = "Hello!"
104+
runBlocking {
105+
assertThat(echo.suspendingCacheableEcho(value)).isEqualTo("$value 0")
106+
assertThat(echo.suspendingCacheableEcho(value)).isEqualTo("$value 0")
107+
assertThat(echo.suspendingCacheableEcho(value)).isEqualTo("$value 0")
108+
assertThat(countingAspect.counter).`as`("aspect applied once").isOne()
109+
110+
assertThat(echo.suspendingCacheableEcho("$value bis")).isEqualTo("$value bis 1")
111+
assertThat(echo.suspendingCacheableEcho("$value bis")).isEqualTo("$value bis 1")
112+
}
113+
assertThat(countingAspect.counter).`as`("aspect applied once per key").isEqualTo(2)
114+
}
115+
96116
@Configuration
97117
@EnableAspectJAutoProxy
98118
@EnableTransactionManagement
119+
@EnableCaching
99120
open class InterceptorConfig {
100121

101122
@Bean
@@ -112,6 +133,11 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
112133
return ReactiveCallCountingTransactionManager()
113134
}
114135

136+
@Bean
137+
open fun cacheManager(): CacheManager {
138+
return ConcurrentMapCacheManager()
139+
}
140+
115141
@Bean
116142
open fun echo(): Echo {
117143
return Echo()
@@ -155,7 +181,7 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
155181
fun logging(joinPoint: ProceedingJoinPoint): Any {
156182
return (joinPoint.proceed(joinPoint.args) as Mono<*>).doOnTerminate {
157183
counter++
158-
}
184+
}.checkpoint("CountingAspect")
159185
}
160186
}
161187

@@ -177,6 +203,15 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
177203
return value
178204
}
179205

206+
open var cacheCounter: Int = 0
207+
208+
@Counting
209+
@Cacheable("something")
210+
open suspend fun suspendingCacheableEcho(value: String): String {
211+
delay(1)
212+
return "$value ${cacheCounter++}"
213+
}
214+
180215
}
181216

182217
}

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

-22
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,9 @@
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;
2522
import org.aopalliance.intercept.MethodInterceptor;
2623
import org.aopalliance.intercept.MethodInvocation;
27-
import org.reactivestreams.Publisher;
2824

29-
import org.springframework.core.CoroutinesUtils;
30-
import org.springframework.core.KotlinDetector;
3125
import org.springframework.lang.Nullable;
3226
import org.springframework.util.Assert;
3327

@@ -58,9 +52,6 @@ public Object invoke(final MethodInvocation invocation) throws Throwable {
5852

5953
CacheOperationInvoker aopAllianceInvoker = () -> {
6054
try {
61-
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isSuspendingFunction(method)) {
62-
return KotlinDelegate.invokeSuspendingFunction(method, invocation.getThis(), invocation.getArguments());
63-
}
6455
return invocation.proceed();
6556
}
6657
catch (Throwable ex) {
@@ -78,17 +69,4 @@ public Object invoke(final MethodInvocation invocation) throws Throwable {
7869
}
7970
}
8071

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

0 commit comments

Comments
 (0)