Support for reactive transactions in TransactionInterceptor

Introduces TransactionManager marker interface for PlatformTransactionManager as well as ReactiveTransactionManager, allowing for a common configuration type in TransactionAspectSupport and TransactionManagementConfigurer.

Closes gh-22590
This commit is contained in:
Juergen Hoeller
2019-05-02 16:23:48 +02:00
parent 8dabb3e626
commit 0be610b0ee
9 changed files with 919 additions and 26 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -43,7 +43,7 @@ import org.springframework.lang.Nullable;
* @see org.springframework.transaction.interceptor.TransactionInterceptor
* @see org.springframework.transaction.interceptor.TransactionProxyFactoryBean
*/
public interface PlatformTransactionManager {
public interface PlatformTransactionManager extends TransactionManager {
/**
* Return a currently active transaction or create a new one, according to

View File

@@ -27,9 +27,8 @@ import reactor.core.publisher.Mono;
* @author Mark Paluch
* @author Juergen Hoeller
* @since 5.2
* @see org.springframework.transaction.interceptor.TransactionProxyFactoryBean
*/
public interface ReactiveTransactionManager {
public interface ReactiveTransactionManager extends TransactionManager {
/**
* Emit a currently active reactive transaction or create a new one, according to

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2002-2019 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.transaction;
/**
* Marker interface for Spring transaction manager implementations,
* either traditional or reactive.
*
* @author Juergen Hoeller
* @since 5.2
* @see PlatformTransactionManager
* @see ReactiveTransactionManager
*/
public interface TransactionManager {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 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.
@@ -27,7 +27,7 @@ import org.springframework.context.annotation.Role;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.lang.Nullable;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.config.TransactionManagementConfigUtils;
import org.springframework.transaction.event.TransactionalEventListenerFactory;
import org.springframework.util.CollectionUtils;
@@ -51,7 +51,7 @@ public abstract class AbstractTransactionManagementConfiguration implements Impo
* Default transaction manager, as configured through a {@link TransactionManagementConfigurer}.
*/
@Nullable
protected PlatformTransactionManager txManager;
protected TransactionManager txManager;
@Override

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2019 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,15 +16,16 @@
package org.springframework.transaction.annotation;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionManager;
/**
* Interface to be implemented by @{@link org.springframework.context.annotation.Configuration
* Configuration} classes annotated with @{@link EnableTransactionManagement} that wish to
* or need to explicitly specify the default {@link PlatformTransactionManager} bean to be
* used for annotation-driven transaction management, as opposed to the default approach
* of a by-type lookup. One reason this might be necessary is if there are two
* {@code PlatformTransactionManager} beans present in the container.
* (or need to) explicitly specify the default {@code PlatformTransactionManager} bean
* (or {@code ReactiveTransactionManager} bean) to be used for annotation-driven
* transaction management, as opposed to the default approach of a by-type lookup.
* One reason this might be necessary is if there are two {@code PlatformTransactionManager}
* beans present in the container.
*
* <p>See @{@link EnableTransactionManagement} for general examples and context;
* see {@link #annotationDrivenTransactionManager()} for detailed instructions.
@@ -40,6 +41,8 @@ import org.springframework.transaction.PlatformTransactionManager;
* @since 3.1
* @see EnableTransactionManagement
* @see org.springframework.context.annotation.Primary
* @see org.springframework.transaction.PlatformTransactionManager
* @see org.springframework.transaction.ReactiveTransactionManager
*/
public interface TransactionManagementConfigurer {
@@ -76,7 +79,9 @@ public interface TransactionManagementConfigurer {
* container as all {@code PlatformTransactionManager} implementations take advantage
* of Spring lifecycle callbacks such as {@code InitializingBean} and
* {@code BeanFactoryAware}.
* @return a {@link org.springframework.transaction.PlatformTransactionManager} or
* {@link org.springframework.transaction.ReactiveTransactionManager} implementation
*/
PlatformTransactionManager annotationDrivenTransactionManager();
TransactionManager annotationDrivenTransactionManager();
}

View File

@@ -23,17 +23,25 @@ import java.util.concurrent.ConcurrentMap;
import io.vavr.control.Try;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
import org.springframework.core.NamedThreadLocal;
import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.lang.Nullable;
import org.springframework.transaction.NoTransactionException;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.ReactiveTransaction;
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.reactive.TransactionContextManager;
import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -86,6 +94,12 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
private static final boolean vavrPresent = ClassUtils.isPresent(
"io.vavr.control.Try", TransactionAspectSupport.class.getClassLoader());
/**
* Reactive Streams API present on the classpath?
*/
private static final boolean reactiveStreamsPresent =
ClassUtils.isPresent("org.reactivestreams.Publisher", TransactionAspectSupport.class.getClassLoader());
/**
* Holder to support the {@code currentTransactionStatus()} method,
* and to support communication between different cooperating advices
@@ -136,11 +150,14 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
protected final Log logger = LogFactory.getLog(getClass());
@Nullable
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
@Nullable
private String transactionManagerBeanName;
@Nullable
private PlatformTransactionManager transactionManager;
private TransactionManager transactionManager;
@Nullable
private TransactionAttributeSource transactionAttributeSource;
@@ -148,12 +165,23 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
@Nullable
private BeanFactory beanFactory;
private final ConcurrentMap<Object, PlatformTransactionManager> transactionManagerCache =
new ConcurrentReferenceHashMap<>(4);
private final ConcurrentMap<Object, Object> transactionManagerCache = new ConcurrentReferenceHashMap<>(4);
protected TransactionAspectSupport() {
if (reactiveStreamsPresent) {
this.reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
}
else {
this.reactiveAdapterRegistry = null;
}
}
/**
* Specify the name of the default transaction manager bean.
* This can either point to a traditional {@link PlatformTransactionManager} or a
* {@link ReactiveTransactionManager} for reactive transaction management.
*/
public void setTransactionManagerBeanName(@Nullable String transactionManagerBeanName) {
this.transactionManagerBeanName = transactionManagerBeanName;
@@ -169,20 +197,24 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
/**
* Specify the <em>default</em> transaction manager to use to drive transactions.
* This can either be a traditional {@link PlatformTransactionManager} or a
* {@link ReactiveTransactionManager} for reactive transaction management.
* <p>The default transaction manager will be used if a <em>qualifier</em>
* has not been declared for a given transaction or if an explicit name for the
* default transaction manager bean has not been specified.
* @see #setTransactionManagerBeanName
*/
public void setTransactionManager(@Nullable PlatformTransactionManager transactionManager) {
public void setTransactionManager(@Nullable TransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
/**
* Return the default transaction manager, or {@code null} if unknown.
* This can either be a traditional {@link PlatformTransactionManager} or a
* {@link ReactiveTransactionManager} for reactive transaction management.
*/
@Nullable
public PlatformTransactionManager getTransactionManager() {
public TransactionManager getTransactionManager() {
return this.transactionManager;
}
@@ -285,6 +317,13 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
if (this.reactiveAdapterRegistry != null) {
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
if (adapter != null) {
return new ReactiveTransactionSupport(adapter).invokeWithinTransaction(method, targetClass, invocation);
}
}
// If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
@@ -398,7 +437,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
protected PlatformTransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
// Do not attempt to lookup tx manager if no tx attributes are set
if (txAttr == null || this.beanFactory == null) {
return getTransactionManager();
return asPlatformTransactionManager(getTransactionManager());
}
String qualifier = txAttr.getQualifier();
@@ -409,9 +448,9 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);
}
else {
PlatformTransactionManager defaultTransactionManager = getTransactionManager();
PlatformTransactionManager defaultTransactionManager = asPlatformTransactionManager(getTransactionManager());
if (defaultTransactionManager == null) {
defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
defaultTransactionManager = asPlatformTransactionManager(this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY));
if (defaultTransactionManager == null) {
defaultTransactionManager = this.beanFactory.getBean(PlatformTransactionManager.class);
this.transactionManagerCache.putIfAbsent(
@@ -423,7 +462,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
}
private PlatformTransactionManager determineQualifiedTransactionManager(BeanFactory beanFactory, String qualifier) {
PlatformTransactionManager txManager = this.transactionManagerCache.get(qualifier);
PlatformTransactionManager txManager = asPlatformTransactionManager(this.transactionManagerCache.get(qualifier));
if (txManager == null) {
txManager = BeanFactoryAnnotationUtils.qualifiedBeanOfType(
beanFactory, PlatformTransactionManager.class, qualifier);
@@ -432,6 +471,18 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
return txManager;
}
@Nullable
private PlatformTransactionManager asPlatformTransactionManager(@Nullable Object transactionManager) {
if (transactionManager == null || transactionManager instanceof PlatformTransactionManager) {
return (PlatformTransactionManager) transactionManager;
}
else {
throw new IllegalStateException(
"Specified transaction manager is not a PlatformTransactionManager: " + transactionManager);
}
}
private String methodIdentification(Method method, @Nullable Class<?> targetClass,
@Nullable TransactionAttribute txAttr) {
@@ -614,7 +665,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
/**
* Opaque object used to hold Transaction information. Subclasses
* Opaque object used to hold transaction information. Subclasses
* must pass it back to methods on this class, but not see its internals.
*/
protected static final class TransactionInfo {
@@ -753,4 +804,286 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
}
}
/**
* Delegate for Reactor-based management of transactional methods with a
* reactive return type.
*/
private class ReactiveTransactionSupport {
private final ReactiveAdapter adapter;
public ReactiveTransactionSupport(ReactiveAdapter adapter) {
this.adapter = adapter;
}
@SuppressWarnings("unchecked")
public Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, InvocationCallback invocation) {
// If the transaction attribute is null, the method is non-transactional.
TransactionAttributeSource tas = getTransactionAttributeSource();
TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
ReactiveTransactionManager tm = determineTransactionManager(txAttr);
String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
// Optimize for Mono
if (Mono.class.isAssignableFrom(method.getReturnType())) {
return TransactionContextManager.currentContext().flatMap(context -> {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
Mono<ReactiveTransactionInfo> txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
return txInfo.flatMap(it -> {
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
Mono<Object> retVal = (Mono) invocation.proceedWithInvocation();
return retVal
.onErrorResume(ex -> completeTransactionAfterThrowing(it, ex).then(Mono.error(ex))).materialize()
.flatMap(signal -> {
if (signal.isOnComplete() || signal.isOnNext()) {
return commitTransactionAfterReturning(it).thenReturn(signal);
}
return Mono.just(signal);
}).dematerialize();
}
catch (Throwable ex) {
// target invocation exception
return completeTransactionAfterThrowing(it, ex).then(Mono.error(ex));
}
});
}).subscriberContext(TransactionContextManager.getOrCreateContext())
.subscriberContext(TransactionContextManager.getOrCreateContextHolder());
}
return TransactionContextManager.currentContext().flatMapMany(context -> {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
Mono<ReactiveTransactionInfo> txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
return txInfo.flatMapMany(it -> {
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
Flux<Object> retVal = Flux.from(this.adapter.toPublisher(invocation.proceedWithInvocation()));
return retVal
.onErrorResume(ex -> completeTransactionAfterThrowing(it, ex).then(Mono.error(ex)))
.materialize().flatMap(signal -> {
if (signal.isOnComplete()) {
return commitTransactionAfterReturning(it).materialize();
}
return Mono.just(signal);
}).dematerialize();
}
catch (Throwable ex) {
// target invocation exception
return completeTransactionAfterThrowing(it, ex).then(Mono.error(ex));
}
});
}).subscriberContext(TransactionContextManager.getOrCreateContext())
.subscriberContext(TransactionContextManager.getOrCreateContextHolder());
}
@Nullable
private ReactiveTransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
// Do not attempt to lookup tx manager if no tx attributes are set
if (txAttr == null || beanFactory == null) {
return asReactiveTransactionManager(getTransactionManager());
}
String qualifier = txAttr.getQualifier();
if (StringUtils.hasText(qualifier)) {
return determineQualifiedTransactionManager(beanFactory, qualifier);
}
else if (StringUtils.hasText(transactionManagerBeanName)) {
return determineQualifiedTransactionManager(beanFactory, transactionManagerBeanName);
}
else {
ReactiveTransactionManager defaultTransactionManager = asReactiveTransactionManager(getTransactionManager());
if (defaultTransactionManager == null) {
defaultTransactionManager = asReactiveTransactionManager(transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY));
if (defaultTransactionManager == null) {
defaultTransactionManager = beanFactory.getBean(ReactiveTransactionManager.class);
transactionManagerCache.putIfAbsent(
DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
}
}
return defaultTransactionManager;
}
}
private ReactiveTransactionManager determineQualifiedTransactionManager(BeanFactory beanFactory, String qualifier) {
ReactiveTransactionManager txManager = asReactiveTransactionManager(transactionManagerCache.get(qualifier));
if (txManager == null) {
txManager = BeanFactoryAnnotationUtils.qualifiedBeanOfType(
beanFactory, ReactiveTransactionManager.class, qualifier);
transactionManagerCache.putIfAbsent(qualifier, txManager);
}
return txManager;
}
@Nullable
private ReactiveTransactionManager asReactiveTransactionManager(@Nullable Object transactionManager) {
if (transactionManager == null || transactionManager instanceof ReactiveTransactionManager) {
return (ReactiveTransactionManager) transactionManager;
}
else {
throw new IllegalStateException(
"Specified transaction manager is not a ReactiveTransactionManager: " + transactionManager);
}
}
@SuppressWarnings("serial")
private Mono<ReactiveTransactionInfo> createTransactionIfNecessary(@Nullable ReactiveTransactionManager tm,
@Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
// If no name specified, apply method identification as transaction name.
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}
TransactionAttribute attrToUse = txAttr;
Mono<ReactiveTransaction> tx = Mono.empty();
if (txAttr != null) {
if (tm != null) {
tx = tm.getReactiveTransaction(txAttr);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
"] because no transaction manager has been configured");
}
}
}
return tx.map(it -> prepareTransactionInfo(tm, attrToUse, joinpointIdentification, it))
.switchIfEmpty(Mono.defer(() -> Mono.just(prepareTransactionInfo(tm, attrToUse, joinpointIdentification, null))));
}
private ReactiveTransactionInfo prepareTransactionInfo(@Nullable ReactiveTransactionManager tm,
@Nullable TransactionAttribute txAttr, String joinpointIdentification,
@Nullable ReactiveTransaction transaction) {
ReactiveTransactionInfo txInfo = new ReactiveTransactionInfo(tm, txAttr, joinpointIdentification);
if (txAttr != null) {
// We need a transaction for this method...
if (logger.isTraceEnabled()) {
logger.trace("Getting transaction for [" + txInfo.getJoinpointIdentification() + "]");
}
// The transaction manager will flag an error if an incompatible tx already exists.
txInfo.newReactiveTransaction(transaction);
}
else {
// The TransactionInfo.hasTransaction() method will return false. We created it only
// to preserve the integrity of the ThreadLocal stack maintained in this class.
if (logger.isTraceEnabled()) {
logger.trace("Don't need to create transaction for [" + joinpointIdentification +
"]: This method isn't transactional.");
}
}
return txInfo;
}
private Mono<Void> commitTransactionAfterReturning(@Nullable ReactiveTransactionInfo txInfo) {
if (txInfo != null && txInfo.getReactiveTransaction() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
}
return txInfo.getTransactionManager().commit(txInfo.getReactiveTransaction());
}
return Mono.empty();
}
private Mono<Void> completeTransactionAfterThrowing(@Nullable ReactiveTransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getReactiveTransaction() != null) {
if (logger.isTraceEnabled()) {
logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
"] after exception: " + ex);
}
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
return txInfo.getTransactionManager().rollback(txInfo.getReactiveTransaction()).onErrorMap(ex2 -> {
logger.error("Application exception overridden by rollback exception", ex);
if (ex2 instanceof TransactionSystemException) {
((TransactionSystemException) ex2).initApplicationException(ex);
}
return ex2;
}
);
}
else {
// We don't roll back on this exception.
// Will still roll back if TransactionStatus.isRollbackOnly() is true.
return txInfo.getTransactionManager().commit(txInfo.getReactiveTransaction()).onErrorMap(ex2 -> {
logger.error("Application exception overridden by commit exception", ex);
if (ex2 instanceof TransactionSystemException) {
((TransactionSystemException) ex2).initApplicationException(ex);
}
return ex2;
}
);
}
}
return Mono.empty();
}
}
/**
* Opaque object used to hold transaction information for reactive methods.
*/
private static final class ReactiveTransactionInfo {
@Nullable
private final ReactiveTransactionManager transactionManager;
@Nullable
private final TransactionAttribute transactionAttribute;
private final String joinpointIdentification;
@Nullable
private ReactiveTransaction reactiveTransaction;
public ReactiveTransactionInfo(@Nullable ReactiveTransactionManager transactionManager,
@Nullable TransactionAttribute transactionAttribute, String joinpointIdentification) {
this.transactionManager = transactionManager;
this.transactionAttribute = transactionAttribute;
this.joinpointIdentification = joinpointIdentification;
}
public ReactiveTransactionManager getTransactionManager() {
Assert.state(this.transactionManager != null, "No ReactiveTransactionManager set");
return this.transactionManager;
}
@Nullable
public TransactionAttribute getTransactionAttribute() {
return this.transactionAttribute;
}
/**
* Return a String representation of this joinpoint (usually a Method call)
* for use in logging.
*/
public String getJoinpointIdentification() {
return this.joinpointIdentification;
}
public void newReactiveTransaction(@Nullable ReactiveTransaction transaction) {
this.reactiveTransaction = transaction;
}
@Nullable
public ReactiveTransaction getReactiveTransaction() {
return this.reactiveTransaction;
}
@Override
public String toString() {
return (this.transactionAttribute != null ? this.transactionAttribute.toString() : "No transaction");
}
}
}