org.springframework.transaction.interceptor.TransactionAspectSupport.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.transaction.interceptor.TransactionAspectSupport.java

Source

/*
 * 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.interceptor;

import java.lang.reflect.Method;
import java.util.Properties;
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;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.StringUtils;

/**
 * Base class for transactional aspects, such as the {@link TransactionInterceptor}
 * or an AspectJ aspect.
 *
 * <p>This enables the underlying Spring transaction infrastructure to be used easily
 * to implement an aspect for any aspect system.
 *
 * <p>Subclasses are responsible for calling methods in this class in the correct order.
 *
 * <p>If no transaction name has been specified in the {@link TransactionAttribute},
 * the exposed name will be the {@code fully-qualified class name + "." + method name}
 * (by default).
 *
 * <p>Uses the <b>Strategy</b> design pattern. A {@link PlatformTransactionManager} or
 * {@link ReactiveTransactionManager} implementation will perform the actual transaction
 * management, and a {@link TransactionAttributeSource} (e.g. annotation-based) is used
 * for determining transaction definitions for a particular class or method.
 *
 * <p>A transaction aspect is serializable if its {@code PlatformTransactionManager}
 * and {@code TransactionAttributeSource} are serializable.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @author Stphane Nicoll
 * @author Sam Brannen
 * @author Mark Paluch
 * @since 1.1
 * @see PlatformTransactionManager
 * @see ReactiveTransactionManager
 * @see #setTransactionManager
 * @see #setTransactionAttributes
 * @see #setTransactionAttributeSource
 */
public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {

    // NOTE: This class must not implement Serializable because it serves as base
    // class for AspectJ aspects (which are not allowed to implement Serializable)!

    /**
     * Key to use to store the default transaction manager.
     */
    private static final Object DEFAULT_TRANSACTION_MANAGER_KEY = new Object();

    /**
     * Vavr library present on the classpath?
     */
    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
     * (e.g. before and after advice) if the aspect involves more than a
     * single method (as will be the case for around advice).
     */
    private static final ThreadLocal<TransactionInfo> transactionInfoHolder = new NamedThreadLocal<>(
            "Current aspect-driven transaction");

    /**
     * Subclasses can use this to return the current TransactionInfo.
     * Only subclasses that cannot handle all operations in one method,
     * such as an AspectJ aspect involving distinct before and after advice,
     * need to use this mechanism to get at the current TransactionInfo.
     * An around advice such as an AOP Alliance MethodInterceptor can hold a
     * reference to the TransactionInfo throughout the aspect method.
     * <p>A TransactionInfo will be returned even if no transaction was created.
     * The {@code TransactionInfo.hasTransaction()} method can be used to query this.
     * <p>To find out about specific transaction characteristics, consider using
     * TransactionSynchronizationManager's {@code isSynchronizationActive()}
     * and/or {@code isActualTransactionActive()} methods.
     * @return the TransactionInfo bound to this thread, or {@code null} if none
     * @see TransactionInfo#hasTransaction()
     * @see org.springframework.transaction.support.TransactionSynchronizationManager#isSynchronizationActive()
     * @see org.springframework.transaction.support.TransactionSynchronizationManager#isActualTransactionActive()
     */
    @Nullable
    protected static TransactionInfo currentTransactionInfo() throws NoTransactionException {
        return transactionInfoHolder.get();
    }

    /**
     * Return the transaction status of the current method invocation.
     * Mainly intended for code that wants to set the current transaction
     * rollback-only but not throw an application exception.
     * @throws NoTransactionException if the transaction info cannot be found,
     * because the method was invoked outside an AOP invocation context
     */
    public static TransactionStatus currentTransactionStatus() throws NoTransactionException {
        TransactionInfo info = currentTransactionInfo();
        if (info == null || info.transactionStatus == null) {
            throw new NoTransactionException("No transaction aspect-managed TransactionStatus in scope");
        }
        return info.transactionStatus;
    }

    protected final Log logger = LogFactory.getLog(getClass());

    @Nullable
    private final ReactiveAdapterRegistry reactiveAdapterRegistry;

    @Nullable
    private String transactionManagerBeanName;

    @Nullable
    private TransactionManager transactionManager;

    @Nullable
    private TransactionAttributeSource transactionAttributeSource;

    @Nullable
    private BeanFactory beanFactory;

    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;
    }

    /**
     * Return the name of the default transaction manager bean.
     */
    @Nullable
    protected final String getTransactionManagerBeanName() {
        return this.transactionManagerBeanName;
    }

    /**
     * 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 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 TransactionManager getTransactionManager() {
        return this.transactionManager;
    }

    /**
     * Set properties with method names as keys and transaction attribute
     * descriptors (parsed via TransactionAttributeEditor) as values:
     * e.g. key = "myMethod", value = "PROPAGATION_REQUIRED,readOnly".
     * <p>Note: Method names are always applied to the target class,
     * no matter if defined in an interface or the class itself.
     * <p>Internally, a NameMatchTransactionAttributeSource will be
     * created from the given properties.
     * @see #setTransactionAttributeSource
     * @see TransactionAttributeEditor
     * @see NameMatchTransactionAttributeSource
     */
    public void setTransactionAttributes(Properties transactionAttributes) {
        NameMatchTransactionAttributeSource tas = new NameMatchTransactionAttributeSource();
        tas.setProperties(transactionAttributes);
        this.transactionAttributeSource = tas;
    }

    /**
     * Set multiple transaction attribute sources which are used to find transaction
     * attributes. Will build a CompositeTransactionAttributeSource for the given sources.
     * @see CompositeTransactionAttributeSource
     * @see MethodMapTransactionAttributeSource
     * @see NameMatchTransactionAttributeSource
     * @see org.springframework.transaction.annotation.AnnotationTransactionAttributeSource
     */
    public void setTransactionAttributeSources(TransactionAttributeSource... transactionAttributeSources) {
        this.transactionAttributeSource = new CompositeTransactionAttributeSource(transactionAttributeSources);
    }

    /**
     * Set the transaction attribute source which is used to find transaction
     * attributes. If specifying a String property value, a PropertyEditor
     * will create a MethodMapTransactionAttributeSource from the value.
     * @see TransactionAttributeSourceEditor
     * @see MethodMapTransactionAttributeSource
     * @see NameMatchTransactionAttributeSource
     * @see org.springframework.transaction.annotation.AnnotationTransactionAttributeSource
     */
    public void setTransactionAttributeSource(@Nullable TransactionAttributeSource transactionAttributeSource) {
        this.transactionAttributeSource = transactionAttributeSource;
    }

    /**
     * Return the transaction attribute source.
     */
    @Nullable
    public TransactionAttributeSource getTransactionAttributeSource() {
        return this.transactionAttributeSource;
    }

    /**
     * Set the BeanFactory to use for retrieving PlatformTransactionManager beans.
     */
    @Override
    public void setBeanFactory(@Nullable BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    /**
     * Return the BeanFactory to use for retrieving PlatformTransactionManager beans.
     */
    @Nullable
    protected final BeanFactory getBeanFactory() {
        return this.beanFactory;
    }

    /**
     * Check that required properties were set.
     */
    @Override
    public void afterPropertiesSet() {
        if (getTransactionManager() == null && this.beanFactory == null) {
            throw new IllegalStateException(
                    "Set the 'transactionManager' property or make sure to run within a BeanFactory "
                            + "containing a PlatformTransactionManager bean!");
        }
        if (getTransactionAttributeSource() == null) {
            throw new IllegalStateException(
                    "Either 'transactionAttributeSource' or 'transactionAttributes' is required: "
                            + "If there are no transactional methods, then don't use a transaction aspect.");
        }
    }

    /**
     * General delegate for around-advice-based subclasses, delegating to several other template
     * methods on this class. Able to handle {@link CallbackPreferringPlatformTransactionManager}
     * as well as regular {@link PlatformTransactionManager} implementations.
     * @param method the Method being invoked
     * @param targetClass the target class that we're invoking the method on
     * @param invocation the callback to use for proceeding with the target invocation
     * @return the return value of the method, if any
     * @throws Throwable propagated from the target invocation
     */
    @Nullable
    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);
        final PlatformTransactionManager tm = determineTransactionManager(txAttr);
        final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

        if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
            // Standard transaction demarcation with getTransaction and commit/rollback calls.
            TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);

            Object retVal;
            try {
                // This is an around advice: Invoke the next interceptor in the chain.
                // This will normally result in a target object being invoked.
                retVal = invocation.proceedWithInvocation();
            } catch (Throwable ex) {
                // target invocation exception
                completeTransactionAfterThrowing(txInfo, ex);
                throw ex;
            } finally {
                cleanupTransactionInfo(txInfo);
            }

            if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
                // Set rollback-only in case of Vavr failure matching our rollback rules...
                TransactionStatus status = txInfo.getTransactionStatus();
                if (status != null && txAttr != null) {
                    retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                }
            }

            commitTransactionAfterReturning(txInfo);
            return retVal;
        }

        else {
            final ThrowableHolder throwableHolder = new ThrowableHolder();

            // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
            try {
                Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr, status -> {
                    TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
                    try {
                        Object retVal = invocation.proceedWithInvocation();
                        if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
                            // Set rollback-only in case of Vavr failure matching our rollback rules...
                            retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
                        }
                        return retVal;
                    } catch (Throwable ex) {
                        if (txAttr.rollbackOn(ex)) {
                            // A RuntimeException: will lead to a rollback.
                            if (ex instanceof RuntimeException) {
                                throw (RuntimeException) ex;
                            } else {
                                throw new ThrowableHolderException(ex);
                            }
                        } else {
                            // A normal return value: will lead to a commit.
                            throwableHolder.throwable = ex;
                            return null;
                        }
                    } finally {
                        cleanupTransactionInfo(txInfo);
                    }
                });

                // Check result state: It might indicate a Throwable to rethrow.
                if (throwableHolder.throwable != null) {
                    throw throwableHolder.throwable;
                }
                return result;
            } catch (ThrowableHolderException ex) {
                throw ex.getCause();
            } catch (TransactionSystemException ex2) {
                if (throwableHolder.throwable != null) {
                    logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                    ex2.initApplicationException(throwableHolder.throwable);
                }
                throw ex2;
            } catch (Throwable ex2) {
                if (throwableHolder.throwable != null) {
                    logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
                }
                throw ex2;
            }
        }
    }

    /**
     * Clear the cache.
     */
    protected void clearTransactionManagerCache() {
        this.transactionManagerCache.clear();
        this.beanFactory = null;
    }

    /**
     * Determine the specific transaction manager to use for the given transaction.
     */
    @Nullable
    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 asPlatformTransactionManager(getTransactionManager());
        }

        String qualifier = txAttr.getQualifier();
        if (StringUtils.hasText(qualifier)) {
            return determineQualifiedTransactionManager(this.beanFactory, qualifier);
        } else if (StringUtils.hasText(this.transactionManagerBeanName)) {
            return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);
        } else {
            PlatformTransactionManager defaultTransactionManager = asPlatformTransactionManager(
                    getTransactionManager());
            if (defaultTransactionManager == null) {
                defaultTransactionManager = asPlatformTransactionManager(
                        this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY));
                if (defaultTransactionManager == null) {
                    defaultTransactionManager = this.beanFactory.getBean(PlatformTransactionManager.class);
                    this.transactionManagerCache.putIfAbsent(DEFAULT_TRANSACTION_MANAGER_KEY,
                            defaultTransactionManager);
                }
            }
            return defaultTransactionManager;
        }
    }

    private PlatformTransactionManager determineQualifiedTransactionManager(BeanFactory beanFactory,
            String qualifier) {
        PlatformTransactionManager txManager = asPlatformTransactionManager(
                this.transactionManagerCache.get(qualifier));
        if (txManager == null) {
            txManager = BeanFactoryAnnotationUtils.qualifiedBeanOfType(beanFactory,
                    PlatformTransactionManager.class, qualifier);
            this.transactionManagerCache.putIfAbsent(qualifier, txManager);
        }
        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) {

        String methodIdentification = methodIdentification(method, targetClass);
        if (methodIdentification == null) {
            if (txAttr instanceof DefaultTransactionAttribute) {
                methodIdentification = ((DefaultTransactionAttribute) txAttr).getDescriptor();
            }
            if (methodIdentification == null) {
                methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass);
            }
        }
        return methodIdentification;
    }

    /**
     * Convenience method to return a String representation of this Method
     * for use in logging. Can be overridden in subclasses to provide a
     * different identifier for the given method.
     * <p>The default implementation returns {@code null}, indicating the
     * use of {@link DefaultTransactionAttribute#getDescriptor()} instead,
     * ending up as {@link ClassUtils#getQualifiedMethodName(Method, Class)}.
     * @param method the method we're interested in
     * @param targetClass the class that the method is being invoked on
     * @return a String representation identifying this method
     * @see org.springframework.util.ClassUtils#getQualifiedMethodName
     */
    @Nullable
    protected String methodIdentification(Method method, @Nullable Class<?> targetClass) {
        return null;
    }

    /**
     * Create a transaction if necessary based on the given TransactionAttribute.
     * <p>Allows callers to perform custom TransactionAttribute lookups through
     * the TransactionAttributeSource.
     * @param txAttr the TransactionAttribute (may be {@code null})
     * @param joinpointIdentification the fully qualified method name
     * (used for monitoring and logging purposes)
     * @return a TransactionInfo object, whether or not a transaction was created.
     * The {@code hasTransaction()} method on TransactionInfo can be used to
     * tell if there was a transaction created.
     * @see #getTransactionAttributeSource()
     */
    @SuppressWarnings("serial")
    protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager 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;
                }
            };
        }

        TransactionStatus status = null;
        if (txAttr != null) {
            if (tm != null) {
                status = tm.getTransaction(txAttr);
            } else {
                if (logger.isDebugEnabled()) {
                    logger.debug("Skipping transactional joinpoint [" + joinpointIdentification
                            + "] because no transaction manager has been configured");
                }
            }
        }
        return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
    }

    /**
     * Prepare a TransactionInfo for the given attribute and status object.
     * @param txAttr the TransactionAttribute (may be {@code null})
     * @param joinpointIdentification the fully qualified method name
     * (used for monitoring and logging purposes)
     * @param status the TransactionStatus for the current transaction
     * @return the prepared TransactionInfo object
     */
    protected TransactionInfo prepareTransactionInfo(@Nullable PlatformTransactionManager tm,
            @Nullable TransactionAttribute txAttr, String joinpointIdentification,
            @Nullable TransactionStatus status) {

        TransactionInfo txInfo = new TransactionInfo(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.newTransactionStatus(status);
        } 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("No need to create transaction for [" + joinpointIdentification
                        + "]: This method is not transactional.");
            }
        }

        // We always bind the TransactionInfo to the thread, even if we didn't create
        // a new transaction here. This guarantees that the TransactionInfo stack
        // will be managed correctly even if no transaction was created by this aspect.
        txInfo.bindToThread();
        return txInfo;
    }

    /**
     * Execute after successful completion of call, but not after an exception was handled.
     * Do nothing if we didn't create a transaction.
     * @param txInfo information about the current transaction
     */
    protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
        if (txInfo != null && txInfo.getTransactionStatus() != null) {
            if (logger.isTraceEnabled()) {
                logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
            }
            txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
        }
    }

    /**
     * Handle a throwable, completing the transaction.
     * We may commit or roll back, depending on the configuration.
     * @param txInfo information about the current transaction
     * @param ex throwable encountered
     */
    protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
        if (txInfo != null && txInfo.getTransactionStatus() != null) {
            if (logger.isTraceEnabled()) {
                logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification()
                        + "] after exception: " + ex);
            }
            if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
                try {
                    txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
                } catch (TransactionSystemException ex2) {
                    logger.error("Application exception overridden by rollback exception", ex);
                    ex2.initApplicationException(ex);
                    throw ex2;
                } catch (RuntimeException | Error ex2) {
                    logger.error("Application exception overridden by rollback exception", ex);
                    throw ex2;
                }
            } else {
                // We don't roll back on this exception.
                // Will still roll back if TransactionStatus.isRollbackOnly() is true.
                try {
                    txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
                } catch (TransactionSystemException ex2) {
                    logger.error("Application exception overridden by commit exception", ex);
                    ex2.initApplicationException(ex);
                    throw ex2;
                } catch (RuntimeException | Error ex2) {
                    logger.error("Application exception overridden by commit exception", ex);
                    throw ex2;
                }
            }
        }
    }

    /**
     * Reset the TransactionInfo ThreadLocal.
     * <p>Call this in all cases: exception or normal return!
     * @param txInfo information about the current transaction (may be {@code null})
     */
    protected void cleanupTransactionInfo(@Nullable TransactionInfo txInfo) {
        if (txInfo != null) {
            txInfo.restoreThreadLocalStatus();
        }
    }

    /**
     * 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 {

        @Nullable
        private final PlatformTransactionManager transactionManager;

        @Nullable
        private final TransactionAttribute transactionAttribute;

        private final String joinpointIdentification;

        @Nullable
        private TransactionStatus transactionStatus;

        @Nullable
        private TransactionInfo oldTransactionInfo;

        public TransactionInfo(@Nullable PlatformTransactionManager transactionManager,
                @Nullable TransactionAttribute transactionAttribute, String joinpointIdentification) {

            this.transactionManager = transactionManager;
            this.transactionAttribute = transactionAttribute;
            this.joinpointIdentification = joinpointIdentification;
        }

        public PlatformTransactionManager getTransactionManager() {
            Assert.state(this.transactionManager != null, "No PlatformTransactionManager 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 newTransactionStatus(@Nullable TransactionStatus status) {
            this.transactionStatus = status;
        }

        @Nullable
        public TransactionStatus getTransactionStatus() {
            return this.transactionStatus;
        }

        /**
         * Return whether a transaction was created by this aspect,
         * or whether we just have a placeholder to keep ThreadLocal stack integrity.
         */
        public boolean hasTransaction() {
            return (this.transactionStatus != null);
        }

        private void bindToThread() {
            // Expose current TransactionStatus, preserving any existing TransactionStatus
            // for restoration after this transaction is complete.
            this.oldTransactionInfo = transactionInfoHolder.get();
            transactionInfoHolder.set(this);
        }

        private void restoreThreadLocalStatus() {
            // Use stack to restore old transaction TransactionInfo.
            // Will be null if none was set.
            transactionInfoHolder.set(this.oldTransactionInfo);
        }

        @Override
        public String toString() {
            return (this.transactionAttribute != null ? this.transactionAttribute.toString() : "No transaction");
        }
    }

    /**
     * Simple callback interface for proceeding with the target invocation.
     * Concrete interceptors/aspects adapt this to their invocation mechanism.
     */
    @FunctionalInterface
    protected interface InvocationCallback {

        Object proceedWithInvocation() throws Throwable;
    }

    /**
     * Internal holder class for a Throwable in a callback transaction model.
     */
    private static class ThrowableHolder {

        @Nullable
        public Throwable throwable;
    }

    /**
     * Internal holder class for a Throwable, used as a RuntimeException to be
     * thrown from a TransactionCallback (and subsequently unwrapped again).
     */
    @SuppressWarnings("serial")
    private static class ThrowableHolderException extends RuntimeException {

        public ThrowableHolderException(Throwable throwable) {
            super(throwable);
        }

        @Override
        public String toString() {
            return getCause().toString();
        }
    }

    /**
     * Inner class to avoid a hard dependency on the Vavr library at runtime.
     */
    private static class VavrDelegate {

        public static boolean isVavrTry(Object retVal) {
            return (retVal instanceof Try);
        }

        public static Object evaluateTryFailure(Object retVal, TransactionAttribute txAttr,
                TransactionStatus status) {
            return ((Try<?>) retVal).onFailure(ex -> {
                if (txAttr.rollbackOn(ex)) {
                    status.setRollbackOnly();
                }
            });
        }
    }

    /**
     * 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;
        }

        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 -> createTransactionIfNecessary(tm, txAttr, joinpointIdentification).flatMap(it -> {
                            try {
                                // Need re-wrapping until we get hold of the exception through usingWhen.
                                return Mono.<Object, ReactiveTransactionInfo>usingWhen(Mono.just(it), txInfo -> {
                                    try {
                                        return (Mono<?>) invocation.proceedWithInvocation();
                                    } catch (Throwable ex) {
                                        return Mono.error(ex);
                                    }
                                }, this::commitTransactionAfterReturning, txInfo -> Mono.empty()).onErrorResume(
                                        ex -> completeTransactionAfterThrowing(it, ex).then(Mono.error(ex)));
                            } catch (Throwable ex) {
                                // target invocation exception
                                return completeTransactionAfterThrowing(it, ex).then(Mono.error(ex));
                            }
                        })).subscriberContext(TransactionContextManager.getOrCreateContext())
                        .subscriberContext(TransactionContextManager.getOrCreateContextHolder());
            }

            // Any other reactive type, typically a Flux
            return this.adapter.fromPublisher(TransactionContextManager.currentContext().flatMapMany(
                    context -> createTransactionIfNecessary(tm, txAttr, joinpointIdentification).flatMapMany(it -> {
                        try {
                            // Need re-wrapping until we get hold of the exception through usingWhen.
                            return Flux.usingWhen(Mono.just(it), txInfo -> {
                                try {
                                    return this.adapter.toPublisher(invocation.proceedWithInvocation());
                                } catch (Throwable ex) {
                                    return Mono.error(ex);
                                }
                            }, this::commitTransactionAfterReturning, txInfo -> Mono.empty()).onErrorResume(
                                    ex -> completeTransactionAfterThrowing(it, ex).then(Mono.error(ex)));
                        } 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");
        }
    }

}