Skip to content

Commit 5429c7a

Browse files
committed
Support suspending functions annotated with @transactional
This commit makes TransactionInterceptor and TransactionAspectSupport Coroutines aware, adapting Reactive transaction support to Coroutines. Suspending functions returning a Flow are handled like Flux, for other return types, they are handled like Mono. Closes gh-23575
1 parent 73eefea commit 5429c7a

File tree

4 files changed

+451
-14
lines changed

4 files changed

+451
-14
lines changed

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

+27-14
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
import java.util.concurrent.ConcurrentMap;
2222

2323
import io.vavr.control.Try;
24-
import kotlin.reflect.KFunction;
25-
import kotlin.reflect.jvm.ReflectJvmMapping;
24+
import kotlin.coroutines.Continuation;
25+
import kotlinx.coroutines.reactive.AwaitKt;
26+
import kotlinx.coroutines.reactive.ReactiveFlowKt;
2627
import org.apache.commons.logging.Log;
2728
import org.apache.commons.logging.LogFactory;
29+
import org.reactivestreams.Publisher;
2830
import reactor.core.publisher.Flux;
2931
import reactor.core.publisher.Mono;
3032

@@ -33,6 +35,7 @@
3335
import org.springframework.beans.factory.InitializingBean;
3436
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
3537
import org.springframework.core.KotlinDetector;
38+
import org.springframework.core.MethodParameter;
3639
import org.springframework.core.NamedThreadLocal;
3740
import org.springframework.core.ReactiveAdapter;
3841
import org.springframework.core.ReactiveAdapterRegistry;
@@ -44,7 +47,6 @@
4447
import org.springframework.transaction.TransactionManager;
4548
import org.springframework.transaction.TransactionStatus;
4649
import org.springframework.transaction.TransactionSystemException;
47-
import org.springframework.transaction.TransactionUsageException;
4850
import org.springframework.transaction.reactive.TransactionContextManager;
4951
import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager;
5052
import org.springframework.util.Assert;
@@ -78,6 +80,7 @@
7880
* @author Stéphane Nicoll
7981
* @author Sam Brannen
8082
* @author Mark Paluch
83+
* @author Sebastien Deleuze
8184
* @since 1.1
8285
* @see PlatformTransactionManager
8386
* @see ReactiveTransactionManager
@@ -96,6 +99,8 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
9699
*/
97100
private static final Object DEFAULT_TRANSACTION_MANAGER_KEY = new Object();
98101

102+
private static final String COROUTINES_FLOW_CLASS_NAME = "kotlinx.coroutines.flow.Flow";
103+
99104
/**
100105
* Vavr library present on the classpath?
101106
*/
@@ -336,21 +341,20 @@ protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targe
336341
final TransactionManager tm = determineTransactionManager(txAttr);
337342

338343
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
344+
boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
345+
boolean hasSuspendingFlowReturnType = isSuspendingFunction && COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName());
339346
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
340-
if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
341-
throw new TransactionUsageException(
342-
"Unsupported annotated transaction on suspending function detected: " + method +
343-
". Use TransactionalOperator.transactional extensions instead.");
344-
}
345-
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
347+
Class<?> reactiveType = (isSuspendingFunction ? (hasSuspendingFlowReturnType ? Flux.class : Mono.class) : method.getReturnType());
348+
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(reactiveType);
346349
if (adapter == null) {
347350
throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
348351
method.getReturnType());
349352
}
350353
return new ReactiveTransactionSupport(adapter);
351354
});
352-
return txSupport.invokeWithinTransaction(
353-
method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
355+
Publisher<?> publisher = (Publisher<?>) txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
356+
return (isSuspendingFunction ? (hasSuspendingFlowReturnType ? KotlinDelegate.asFlow(publisher) :
357+
KotlinDelegate.awaitSingleOrNull(publisher, ((CoroutinesInvocationCallback) invocation).getContinuation())) : publisher);
354358
}
355359

356360
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
@@ -785,6 +789,11 @@ protected interface InvocationCallback {
785789
Object proceedWithInvocation() throws Throwable;
786790
}
787791

792+
protected interface CoroutinesInvocationCallback extends InvocationCallback {
793+
794+
Object getContinuation();
795+
}
796+
788797

789798
/**
790799
* Internal holder class for a Throwable in a callback transaction model.
@@ -837,9 +846,13 @@ public static Object evaluateTryFailure(Object retVal, TransactionAttribute txAt
837846
*/
838847
private static class KotlinDelegate {
839848

840-
private static boolean isSuspend(Method method) {
841-
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
842-
return function != null && function.isSuspend();
849+
private static Object asFlow(Publisher<?> publisher) {
850+
return ReactiveFlowKt.asFlow(publisher);
851+
}
852+
853+
@SuppressWarnings("unchecked")
854+
private static Object awaitSingleOrNull(Publisher<?> publisher, Object continuation) {
855+
return AwaitKt.awaitSingleOrNull(publisher, (Continuation<Object>) continuation);
843856
}
844857
}
845858

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

+16
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727

2828
import org.springframework.aop.support.AopUtils;
2929
import org.springframework.beans.factory.BeanFactory;
30+
import org.springframework.core.CoroutinesUtils;
31+
import org.springframework.core.KotlinDetector;
3032
import org.springframework.lang.Nullable;
3133
import org.springframework.transaction.PlatformTransactionManager;
3234
import org.springframework.transaction.TransactionManager;
@@ -46,6 +48,7 @@
4648
*
4749
* @author Rod Johnson
4850
* @author Juergen Hoeller
51+
* @author Sebastien Deleuze
4952
* @see TransactionProxyFactoryBean
5053
* @see org.springframework.aop.framework.ProxyFactoryBean
5154
* @see org.springframework.aop.framework.ProxyFactory
@@ -115,6 +118,19 @@ public Object invoke(MethodInvocation invocation) throws Throwable {
115118
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
116119

117120
// Adapt to TransactionAspectSupport's invokeWithinTransaction...
121+
if (KotlinDetector.isSuspendingFunction(invocation.getMethod())) {
122+
InvocationCallback callback = new CoroutinesInvocationCallback() {
123+
@Override
124+
public Object proceedWithInvocation() {
125+
return CoroutinesUtils.invokeSuspendingFunction(invocation.getMethod(), invocation.getThis(), invocation.getArguments());
126+
}
127+
@Override
128+
public Object getContinuation() {
129+
return invocation.getArguments()[invocation.getArguments().length - 1];
130+
}
131+
};
132+
return invokeWithinTransaction(invocation.getMethod(), targetClass, callback);
133+
}
118134
return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
119135
}
120136

0 commit comments

Comments
 (0)