Skip to content

Commit 1d890a8

Browse files
committed
Make coroutines with custom AOP aspects work with @Transactional
Previous to this change, the transactional aspect would supersed the user-defined AspectJ aspect, shortcircuiting to calling the original Kotlin suspending function. This change simplifies the TransactionAspectSupport way of dealing with transactional coroutines, thanks to the fact that lower level support for AOP has been introduced in c8169e5. Closes gh-33095
1 parent 3ccaefe commit 1d890a8

File tree

3 files changed

+62
-58
lines changed

3 files changed

+62
-58
lines changed

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

+60-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import kotlinx.coroutines.delay
2020
import kotlinx.coroutines.runBlocking
2121
import org.aopalliance.intercept.MethodInterceptor
2222
import org.aopalliance.intercept.MethodInvocation
23+
import org.aspectj.lang.ProceedingJoinPoint
24+
import org.aspectj.lang.annotation.Around
25+
import org.aspectj.lang.annotation.Aspect
2326
import org.assertj.core.api.Assertions.assertThat
2427
import org.junit.jupiter.api.Test
2528
import org.springframework.aop.framework.autoproxy.AspectJAutoProxyInterceptorKotlinIntegrationTests.InterceptorConfig
@@ -28,10 +31,18 @@ import org.springframework.beans.factory.annotation.Autowired
2831
import org.springframework.context.annotation.Bean
2932
import org.springframework.context.annotation.Configuration
3033
import org.springframework.context.annotation.EnableAspectJAutoProxy
34+
import org.springframework.stereotype.Component
3135
import org.springframework.test.annotation.DirtiesContext
3236
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig
37+
import org.springframework.transaction.annotation.EnableTransactionManagement
38+
import org.springframework.transaction.annotation.Transactional
39+
import org.springframework.transaction.testfixture.ReactiveCallCountingTransactionManager
3340
import reactor.core.publisher.Mono
3441
import java.lang.reflect.Method
42+
import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
43+
import kotlin.annotation.AnnotationTarget.CLASS
44+
import kotlin.annotation.AnnotationTarget.FUNCTION
45+
import kotlin.annotation.AnnotationTarget.TYPE
3546

3647

3748
/**
@@ -43,7 +54,9 @@ import java.lang.reflect.Method
4354
class AspectJAutoProxyInterceptorKotlinIntegrationTests(
4455
@Autowired val echo: Echo,
4556
@Autowired val firstAdvisor: TestPointcutAdvisor,
46-
@Autowired val secondAdvisor: TestPointcutAdvisor) {
57+
@Autowired val secondAdvisor: TestPointcutAdvisor,
58+
@Autowired val countingAspect: CountingAspect,
59+
@Autowired val reactiveTransactionManager: ReactiveCallCountingTransactionManager) {
4760

4861
@Test
4962
fun `Multiple interceptors with regular function`() {
@@ -67,8 +80,22 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
6780
assertThat(secondAdvisor.interceptor.invocations).singleElement().matches { Mono::class.java.isAssignableFrom(it) }
6881
}
6982

83+
@Test // gh-33095
84+
fun `Aspect and reactive transactional with suspending function`() {
85+
assertThat(countingAspect.counter).isZero()
86+
assertThat(reactiveTransactionManager.commits).isZero()
87+
val value = "Hello!"
88+
runBlocking {
89+
assertThat(echo.suspendingTransactionalEcho(value)).isEqualTo(value)
90+
}
91+
assertThat(countingAspect.counter).`as`("aspect applied").isOne()
92+
assertThat(reactiveTransactionManager.begun).isOne()
93+
assertThat(reactiveTransactionManager.commits).`as`("transactional applied").isOne()
94+
}
95+
7096
@Configuration
7197
@EnableAspectJAutoProxy
98+
@EnableTransactionManagement
7299
open class InterceptorConfig {
73100

74101
@Bean
@@ -77,6 +104,13 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
77104
@Bean
78105
open fun secondAdvisor() = TestPointcutAdvisor().apply { order = 1 }
79106

107+
@Bean
108+
open fun countingAspect() = CountingAspect()
109+
110+
@Bean
111+
open fun transactionManager(): ReactiveCallCountingTransactionManager {
112+
return ReactiveCallCountingTransactionManager()
113+
}
80114

81115
@Bean
82116
open fun echo(): Echo {
@@ -107,6 +141,24 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
107141
}
108142
}
109143

144+
@Target(CLASS, FUNCTION, ANNOTATION_CLASS, TYPE)
145+
@Retention(AnnotationRetention.RUNTIME)
146+
annotation class Counting()
147+
148+
@Aspect
149+
@Component
150+
class CountingAspect {
151+
152+
var counter: Long = 0
153+
154+
@Around("@annotation(org.springframework.aop.framework.autoproxy.AspectJAutoProxyInterceptorKotlinIntegrationTests.Counting)")
155+
fun logging(joinPoint: ProceedingJoinPoint): Any {
156+
return (joinPoint.proceed(joinPoint.args) as Mono<*>).doOnTerminate {
157+
counter++
158+
}
159+
}
160+
}
161+
110162
open class Echo {
111163

112164
open fun echo(value: String): String {
@@ -118,6 +170,13 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
118170
return value
119171
}
120172

173+
@Transactional
174+
@Counting
175+
open suspend fun suspendingTransactionalEcho(value: String): String {
176+
delay(1)
177+
return value
178+
}
179+
121180
}
122181

123182
}

spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java

+1-42
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,15 @@
2323
import java.util.concurrent.Future;
2424

2525
import io.vavr.control.Try;
26-
import kotlin.coroutines.Continuation;
27-
import kotlin.coroutines.CoroutineContext;
28-
import kotlinx.coroutines.Job;
2926
import org.apache.commons.logging.Log;
3027
import org.apache.commons.logging.LogFactory;
31-
import org.reactivestreams.Publisher;
3228
import reactor.core.publisher.Flux;
3329
import reactor.core.publisher.Mono;
3430

3531
import org.springframework.beans.factory.BeanFactory;
3632
import org.springframework.beans.factory.BeanFactoryAware;
3733
import org.springframework.beans.factory.InitializingBean;
3834
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
39-
import org.springframework.core.CoroutinesUtils;
4035
import org.springframework.core.KotlinDetector;
4136
import org.springframework.core.MethodParameter;
4237
import org.springframework.core.NamedThreadLocal;
@@ -355,10 +350,6 @@ protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targe
355350
boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
356351
boolean hasSuspendingFlowReturnType = isSuspendingFunction &&
357352
COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName());
358-
if (isSuspendingFunction && !(invocation instanceof CoroutinesInvocationCallback)) {
359-
throw new IllegalStateException("Coroutines invocation not supported: " + method);
360-
}
361-
CoroutinesInvocationCallback corInv = (isSuspendingFunction ? (CoroutinesInvocationCallback) invocation : null);
362353

363354
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
364355
Class<?> reactiveType =
@@ -371,11 +362,7 @@ protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targe
371362
return new ReactiveTransactionSupport(adapter);
372363
});
373364

374-
InvocationCallback callback = invocation;
375-
if (corInv != null) {
376-
callback = () -> KotlinDelegate.invokeSuspendingFunction(method, corInv);
377-
}
378-
return txSupport.invokeWithinTransaction(method, targetClass, callback, txAttr, rtm);
365+
return txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, rtm);
379366
}
380367

381368
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
@@ -829,22 +816,6 @@ protected interface InvocationCallback {
829816
}
830817

831818

832-
/**
833-
* Coroutines-supporting extension of the callback interface.
834-
*/
835-
protected interface CoroutinesInvocationCallback extends InvocationCallback {
836-
837-
Object getTarget();
838-
839-
Object[] getArguments();
840-
841-
default Object getContinuation() {
842-
Object[] args = getArguments();
843-
return args[args.length - 1];
844-
}
845-
}
846-
847-
848819
/**
849820
* Internal holder class for a Throwable in a callback transaction model.
850821
*/
@@ -891,18 +862,6 @@ public static Object evaluateTryFailure(Object retVal, TransactionAttribute txAt
891862
}
892863
}
893864

894-
/**
895-
* Inner class to avoid a hard dependency on Kotlin at runtime.
896-
*/
897-
private static class KotlinDelegate {
898-
899-
public static Publisher<?> invokeSuspendingFunction(Method method, CoroutinesInvocationCallback callback) {
900-
CoroutineContext coroutineContext = ((Continuation<?>) callback.getContinuation()).getContext().minusKey(Job.Key);
901-
return CoroutinesUtils.invokeSuspendingFunction(coroutineContext, method, callback.getTarget(), callback.getArguments());
902-
}
903-
904-
}
905-
906865

907866
/**
908867
* Delegate for Reactor-based management of transactional methods with a

spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionInterceptor.java

+1-15
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable {
116116
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
117117

118118
// Adapt to TransactionAspectSupport's invokeWithinTransaction...
119-
return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {
120-
@Override
121-
@Nullable
122-
public Object proceedWithInvocation() throws Throwable {
123-
return invocation.proceed();
124-
}
125-
@Override
126-
public Object getTarget() {
127-
return invocation.getThis();
128-
}
129-
@Override
130-
public Object[] getArguments() {
131-
return invocation.getArguments();
132-
}
133-
});
119+
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
134120
}
135121

136122

0 commit comments

Comments
 (0)