001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.transaction.interceptor;
018
019import java.lang.reflect.Method;
020import java.util.Properties;
021import java.util.concurrent.ConcurrentMap;
022
023import io.vavr.control.Try;
024import kotlin.reflect.KFunction;
025import kotlin.reflect.jvm.ReflectJvmMapping;
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import reactor.core.publisher.Flux;
029import reactor.core.publisher.Mono;
030
031import org.springframework.beans.factory.BeanFactory;
032import org.springframework.beans.factory.BeanFactoryAware;
033import org.springframework.beans.factory.InitializingBean;
034import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils;
035import org.springframework.core.KotlinDetector;
036import org.springframework.core.NamedThreadLocal;
037import org.springframework.core.ReactiveAdapter;
038import org.springframework.core.ReactiveAdapterRegistry;
039import org.springframework.lang.Nullable;
040import org.springframework.transaction.NoTransactionException;
041import org.springframework.transaction.PlatformTransactionManager;
042import org.springframework.transaction.ReactiveTransaction;
043import org.springframework.transaction.ReactiveTransactionManager;
044import org.springframework.transaction.TransactionManager;
045import org.springframework.transaction.TransactionStatus;
046import org.springframework.transaction.TransactionSystemException;
047import org.springframework.transaction.TransactionUsageException;
048import org.springframework.transaction.reactive.TransactionContextManager;
049import org.springframework.transaction.support.CallbackPreferringPlatformTransactionManager;
050import org.springframework.util.Assert;
051import org.springframework.util.ClassUtils;
052import org.springframework.util.ConcurrentReferenceHashMap;
053import org.springframework.util.StringUtils;
054
055/**
056 * Base class for transactional aspects, such as the {@link TransactionInterceptor}
057 * or an AspectJ aspect.
058 *
059 * <p>This enables the underlying Spring transaction infrastructure to be used easily
060 * to implement an aspect for any aspect system.
061 *
062 * <p>Subclasses are responsible for calling methods in this class in the correct order.
063 *
064 * <p>If no transaction name has been specified in the {@link TransactionAttribute},
065 * the exposed name will be the {@code fully-qualified class name + "." + method name}
066 * (by default).
067 *
068 * <p>Uses the <b>Strategy</b> design pattern. A {@link PlatformTransactionManager} or
069 * {@link ReactiveTransactionManager} implementation will perform the actual transaction
070 * management, and a {@link TransactionAttributeSource} (e.g. annotation-based) is used
071 * for determining transaction definitions for a particular class or method.
072 *
073 * <p>A transaction aspect is serializable if its {@code TransactionManager} and
074 * {@code TransactionAttributeSource} are serializable.
075 *
076 * @author Rod Johnson
077 * @author Juergen Hoeller
078 * @author St茅phane Nicoll
079 * @author Sam Brannen
080 * @author Mark Paluch
081 * @since 1.1
082 * @see PlatformTransactionManager
083 * @see ReactiveTransactionManager
084 * @see #setTransactionManager
085 * @see #setTransactionAttributes
086 * @see #setTransactionAttributeSource
087 */
088public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
089
090        // NOTE: This class must not implement Serializable because it serves as base
091        // class for AspectJ aspects (which are not allowed to implement Serializable)!
092
093
094        /**
095         * Key to use to store the default transaction manager.
096         */
097        private static final Object DEFAULT_TRANSACTION_MANAGER_KEY = new Object();
098
099        /**
100         * Vavr library present on the classpath?
101         */
102        private static final boolean vavrPresent = ClassUtils.isPresent(
103                        "io.vavr.control.Try", TransactionAspectSupport.class.getClassLoader());
104
105        /**
106         * Reactive Streams API present on the classpath?
107         */
108        private static final boolean reactiveStreamsPresent =
109                        ClassUtils.isPresent("org.reactivestreams.Publisher", TransactionAspectSupport.class.getClassLoader());
110
111        /**
112         * Holder to support the {@code currentTransactionStatus()} method,
113         * and to support communication between different cooperating advices
114         * (e.g. before and after advice) if the aspect involves more than a
115         * single method (as will be the case for around advice).
116         */
117        private static final ThreadLocal<TransactionInfo> transactionInfoHolder =
118                        new NamedThreadLocal<>("Current aspect-driven transaction");
119
120
121        /**
122         * Subclasses can use this to return the current TransactionInfo.
123         * Only subclasses that cannot handle all operations in one method,
124         * such as an AspectJ aspect involving distinct before and after advice,
125         * need to use this mechanism to get at the current TransactionInfo.
126         * An around advice such as an AOP Alliance MethodInterceptor can hold a
127         * reference to the TransactionInfo throughout the aspect method.
128         * <p>A TransactionInfo will be returned even if no transaction was created.
129         * The {@code TransactionInfo.hasTransaction()} method can be used to query this.
130         * <p>To find out about specific transaction characteristics, consider using
131         * TransactionSynchronizationManager's {@code isSynchronizationActive()}
132         * and/or {@code isActualTransactionActive()} methods.
133         * @return the TransactionInfo bound to this thread, or {@code null} if none
134         * @see TransactionInfo#hasTransaction()
135         * @see org.springframework.transaction.support.TransactionSynchronizationManager#isSynchronizationActive()
136         * @see org.springframework.transaction.support.TransactionSynchronizationManager#isActualTransactionActive()
137         */
138        @Nullable
139        protected static TransactionInfo currentTransactionInfo() throws NoTransactionException {
140                return transactionInfoHolder.get();
141        }
142
143        /**
144         * Return the transaction status of the current method invocation.
145         * Mainly intended for code that wants to set the current transaction
146         * rollback-only but not throw an application exception.
147         * @throws NoTransactionException if the transaction info cannot be found,
148         * because the method was invoked outside an AOP invocation context
149         */
150        public static TransactionStatus currentTransactionStatus() throws NoTransactionException {
151                TransactionInfo info = currentTransactionInfo();
152                if (info == null || info.transactionStatus == null) {
153                        throw new NoTransactionException("No transaction aspect-managed TransactionStatus in scope");
154                }
155                return info.transactionStatus;
156        }
157
158
159        protected final Log logger = LogFactory.getLog(getClass());
160
161        @Nullable
162        private final ReactiveAdapterRegistry reactiveAdapterRegistry;
163
164        @Nullable
165        private String transactionManagerBeanName;
166
167        @Nullable
168        private TransactionManager transactionManager;
169
170        @Nullable
171        private TransactionAttributeSource transactionAttributeSource;
172
173        @Nullable
174        private BeanFactory beanFactory;
175
176        private final ConcurrentMap<Object, TransactionManager> transactionManagerCache =
177                        new ConcurrentReferenceHashMap<>(4);
178
179        private final ConcurrentMap<Method, ReactiveTransactionSupport> transactionSupportCache =
180                        new ConcurrentReferenceHashMap<>(1024);
181
182
183        protected TransactionAspectSupport() {
184                if (reactiveStreamsPresent) {
185                        this.reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
186                }
187                else {
188                        this.reactiveAdapterRegistry = null;
189                }
190        }
191
192
193        /**
194         * Specify the name of the default transaction manager bean.
195         * <p>This can either point to a traditional {@link PlatformTransactionManager} or a
196         * {@link ReactiveTransactionManager} for reactive transaction management.
197         */
198        public void setTransactionManagerBeanName(@Nullable String transactionManagerBeanName) {
199                this.transactionManagerBeanName = transactionManagerBeanName;
200        }
201
202        /**
203         * Return the name of the default transaction manager bean.
204         */
205        @Nullable
206        protected final String getTransactionManagerBeanName() {
207                return this.transactionManagerBeanName;
208        }
209
210        /**
211         * Specify the <em>default</em> transaction manager to use to drive transactions.
212         * <p>This can either be a traditional {@link PlatformTransactionManager} or a
213         * {@link ReactiveTransactionManager} for reactive transaction management.
214         * <p>The default transaction manager will be used if a <em>qualifier</em>
215         * has not been declared for a given transaction or if an explicit name for the
216         * default transaction manager bean has not been specified.
217         * @see #setTransactionManagerBeanName
218         */
219        public void setTransactionManager(@Nullable TransactionManager transactionManager) {
220                this.transactionManager = transactionManager;
221        }
222
223        /**
224         * Return the default transaction manager, or {@code null} if unknown.
225         * <p>This can either be a traditional {@link PlatformTransactionManager} or a
226         * {@link ReactiveTransactionManager} for reactive transaction management.
227         */
228        @Nullable
229        public TransactionManager getTransactionManager() {
230                return this.transactionManager;
231        }
232
233        /**
234         * Set properties with method names as keys and transaction attribute
235         * descriptors (parsed via TransactionAttributeEditor) as values:
236         * e.g. key = "myMethod", value = "PROPAGATION_REQUIRED,readOnly".
237         * <p>Note: Method names are always applied to the target class,
238         * no matter if defined in an interface or the class itself.
239         * <p>Internally, a NameMatchTransactionAttributeSource will be
240         * created from the given properties.
241         * @see #setTransactionAttributeSource
242         * @see TransactionAttributeEditor
243         * @see NameMatchTransactionAttributeSource
244         */
245        public void setTransactionAttributes(Properties transactionAttributes) {
246                NameMatchTransactionAttributeSource tas = new NameMatchTransactionAttributeSource();
247                tas.setProperties(transactionAttributes);
248                this.transactionAttributeSource = tas;
249        }
250
251        /**
252         * Set multiple transaction attribute sources which are used to find transaction
253         * attributes. Will build a CompositeTransactionAttributeSource for the given sources.
254         * @see CompositeTransactionAttributeSource
255         * @see MethodMapTransactionAttributeSource
256         * @see NameMatchTransactionAttributeSource
257         * @see org.springframework.transaction.annotation.AnnotationTransactionAttributeSource
258         */
259        public void setTransactionAttributeSources(TransactionAttributeSource... transactionAttributeSources) {
260                this.transactionAttributeSource = new CompositeTransactionAttributeSource(transactionAttributeSources);
261        }
262
263        /**
264         * Set the transaction attribute source which is used to find transaction
265         * attributes. If specifying a String property value, a PropertyEditor
266         * will create a MethodMapTransactionAttributeSource from the value.
267         * @see TransactionAttributeSourceEditor
268         * @see MethodMapTransactionAttributeSource
269         * @see NameMatchTransactionAttributeSource
270         * @see org.springframework.transaction.annotation.AnnotationTransactionAttributeSource
271         */
272        public void setTransactionAttributeSource(@Nullable TransactionAttributeSource transactionAttributeSource) {
273                this.transactionAttributeSource = transactionAttributeSource;
274        }
275
276        /**
277         * Return the transaction attribute source.
278         */
279        @Nullable
280        public TransactionAttributeSource getTransactionAttributeSource() {
281                return this.transactionAttributeSource;
282        }
283
284        /**
285         * Set the BeanFactory to use for retrieving {@code TransactionManager} beans.
286         */
287        @Override
288        public void setBeanFactory(@Nullable BeanFactory beanFactory) {
289                this.beanFactory = beanFactory;
290        }
291
292        /**
293         * Return the BeanFactory to use for retrieving {@code TransactionManager} beans.
294         */
295        @Nullable
296        protected final BeanFactory getBeanFactory() {
297                return this.beanFactory;
298        }
299
300        /**
301         * Check that required properties were set.
302         */
303        @Override
304        public void afterPropertiesSet() {
305                if (getTransactionManager() == null && this.beanFactory == null) {
306                        throw new IllegalStateException(
307                                        "Set the 'transactionManager' property or make sure to run within a BeanFactory " +
308                                        "containing a TransactionManager bean!");
309                }
310                if (getTransactionAttributeSource() == null) {
311                        throw new IllegalStateException(
312                                        "Either 'transactionAttributeSource' or 'transactionAttributes' is required: " +
313                                        "If there are no transactional methods, then don't use a transaction aspect.");
314                }
315        }
316
317
318        /**
319         * General delegate for around-advice-based subclasses, delegating to several other template
320         * methods on this class. Able to handle {@link CallbackPreferringPlatformTransactionManager}
321         * as well as regular {@link PlatformTransactionManager} implementations and
322         * {@link ReactiveTransactionManager} implementations for reactive return types.
323         * @param method the Method being invoked
324         * @param targetClass the target class that we're invoking the method on
325         * @param invocation the callback to use for proceeding with the target invocation
326         * @return the return value of the method, if any
327         * @throws Throwable propagated from the target invocation
328         */
329        @Nullable
330        protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
331                        final InvocationCallback invocation) throws Throwable {
332
333                // If the transaction attribute is null, the method is non-transactional.
334                TransactionAttributeSource tas = getTransactionAttributeSource();
335                final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
336                final TransactionManager tm = determineTransactionManager(txAttr);
337
338                if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
339                        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());
346                                if (adapter == null) {
347                                        throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
348                                                        method.getReturnType());
349                                }
350                                return new ReactiveTransactionSupport(adapter);
351                        });
352                        return txSupport.invokeWithinTransaction(
353                                        method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
354                }
355
356                PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
357                final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
358
359                if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
360                        // Standard transaction demarcation with getTransaction and commit/rollback calls.
361                        TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
362
363                        Object retVal;
364                        try {
365                                // This is an around advice: Invoke the next interceptor in the chain.
366                                // This will normally result in a target object being invoked.
367                                retVal = invocation.proceedWithInvocation();
368                        }
369                        catch (Throwable ex) {
370                                // target invocation exception
371                                completeTransactionAfterThrowing(txInfo, ex);
372                                throw ex;
373                        }
374                        finally {
375                                cleanupTransactionInfo(txInfo);
376                        }
377
378                        if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
379                                // Set rollback-only in case of Vavr failure matching our rollback rules...
380                                TransactionStatus status = txInfo.getTransactionStatus();
381                                if (status != null && txAttr != null) {
382                                        retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
383                                }
384                        }
385
386                        commitTransactionAfterReturning(txInfo);
387                        return retVal;
388                }
389
390                else {
391                        Object result;
392                        final ThrowableHolder throwableHolder = new ThrowableHolder();
393
394                        // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
395                        try {
396                                result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
397                                        TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
398                                        try {
399                                                Object retVal = invocation.proceedWithInvocation();
400                                                if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
401                                                        // Set rollback-only in case of Vavr failure matching our rollback rules...
402                                                        retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
403                                                }
404                                                return retVal;
405                                        }
406                                        catch (Throwable ex) {
407                                                if (txAttr.rollbackOn(ex)) {
408                                                        // A RuntimeException: will lead to a rollback.
409                                                        if (ex instanceof RuntimeException) {
410                                                                throw (RuntimeException) ex;
411                                                        }
412                                                        else {
413                                                                throw new ThrowableHolderException(ex);
414                                                        }
415                                                }
416                                                else {
417                                                        // A normal return value: will lead to a commit.
418                                                        throwableHolder.throwable = ex;
419                                                        return null;
420                                                }
421                                        }
422                                        finally {
423                                                cleanupTransactionInfo(txInfo);
424                                        }
425                                });
426                        }
427                        catch (ThrowableHolderException ex) {
428                                throw ex.getCause();
429                        }
430                        catch (TransactionSystemException ex2) {
431                                if (throwableHolder.throwable != null) {
432                                        logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
433                                        ex2.initApplicationException(throwableHolder.throwable);
434                                }
435                                throw ex2;
436                        }
437                        catch (Throwable ex2) {
438                                if (throwableHolder.throwable != null) {
439                                        logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
440                                }
441                                throw ex2;
442                        }
443
444                        // Check result state: It might indicate a Throwable to rethrow.
445                        if (throwableHolder.throwable != null) {
446                                throw throwableHolder.throwable;
447                        }
448                        return result;
449                }
450        }
451
452        /**
453         * Clear the transaction manager cache.
454         */
455        protected void clearTransactionManagerCache() {
456                this.transactionManagerCache.clear();
457                this.beanFactory = null;
458        }
459
460        /**
461         * Determine the specific transaction manager to use for the given transaction.
462         */
463        @Nullable
464        protected TransactionManager determineTransactionManager(@Nullable TransactionAttribute txAttr) {
465                // Do not attempt to lookup tx manager if no tx attributes are set
466                if (txAttr == null || this.beanFactory == null) {
467                        return getTransactionManager();
468                }
469
470                String qualifier = txAttr.getQualifier();
471                if (StringUtils.hasText(qualifier)) {
472                        return determineQualifiedTransactionManager(this.beanFactory, qualifier);
473                }
474                else if (StringUtils.hasText(this.transactionManagerBeanName)) {
475                        return determineQualifiedTransactionManager(this.beanFactory, this.transactionManagerBeanName);
476                }
477                else {
478                        TransactionManager defaultTransactionManager = getTransactionManager();
479                        if (defaultTransactionManager == null) {
480                                defaultTransactionManager = this.transactionManagerCache.get(DEFAULT_TRANSACTION_MANAGER_KEY);
481                                if (defaultTransactionManager == null) {
482                                        defaultTransactionManager = this.beanFactory.getBean(TransactionManager.class);
483                                        this.transactionManagerCache.putIfAbsent(
484                                                        DEFAULT_TRANSACTION_MANAGER_KEY, defaultTransactionManager);
485                                }
486                        }
487                        return defaultTransactionManager;
488                }
489        }
490
491        private TransactionManager determineQualifiedTransactionManager(BeanFactory beanFactory, String qualifier) {
492                TransactionManager txManager = this.transactionManagerCache.get(qualifier);
493                if (txManager == null) {
494                        txManager = BeanFactoryAnnotationUtils.qualifiedBeanOfType(
495                                        beanFactory, TransactionManager.class, qualifier);
496                        this.transactionManagerCache.putIfAbsent(qualifier, txManager);
497                }
498                return txManager;
499        }
500
501
502        @Nullable
503        private PlatformTransactionManager asPlatformTransactionManager(@Nullable Object transactionManager) {
504                if (transactionManager == null || transactionManager instanceof PlatformTransactionManager) {
505                        return (PlatformTransactionManager) transactionManager;
506                }
507                else {
508                        throw new IllegalStateException(
509                                        "Specified transaction manager is not a PlatformTransactionManager: " + transactionManager);
510                }
511        }
512
513        private String methodIdentification(Method method, @Nullable Class<?> targetClass,
514                        @Nullable TransactionAttribute txAttr) {
515
516                String methodIdentification = methodIdentification(method, targetClass);
517                if (methodIdentification == null) {
518                        if (txAttr instanceof DefaultTransactionAttribute) {
519                                methodIdentification = ((DefaultTransactionAttribute) txAttr).getDescriptor();
520                        }
521                        if (methodIdentification == null) {
522                                methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass);
523                        }
524                }
525                return methodIdentification;
526        }
527
528        /**
529         * Convenience method to return a String representation of this Method
530         * for use in logging. Can be overridden in subclasses to provide a
531         * different identifier for the given method.
532         * <p>The default implementation returns {@code null}, indicating the
533         * use of {@link DefaultTransactionAttribute#getDescriptor()} instead,
534         * ending up as {@link ClassUtils#getQualifiedMethodName(Method, Class)}.
535         * @param method the method we're interested in
536         * @param targetClass the class that the method is being invoked on
537         * @return a String representation identifying this method
538         * @see org.springframework.util.ClassUtils#getQualifiedMethodName
539         */
540        @Nullable
541        protected String methodIdentification(Method method, @Nullable Class<?> targetClass) {
542                return null;
543        }
544
545        /**
546         * Create a transaction if necessary based on the given TransactionAttribute.
547         * <p>Allows callers to perform custom TransactionAttribute lookups through
548         * the TransactionAttributeSource.
549         * @param txAttr the TransactionAttribute (may be {@code null})
550         * @param joinpointIdentification the fully qualified method name
551         * (used for monitoring and logging purposes)
552         * @return a TransactionInfo object, whether or not a transaction was created.
553         * The {@code hasTransaction()} method on TransactionInfo can be used to
554         * tell if there was a transaction created.
555         * @see #getTransactionAttributeSource()
556         */
557        @SuppressWarnings("serial")
558        protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm,
559                        @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
560
561                // If no name specified, apply method identification as transaction name.
562                if (txAttr != null && txAttr.getName() == null) {
563                        txAttr = new DelegatingTransactionAttribute(txAttr) {
564                                @Override
565                                public String getName() {
566                                        return joinpointIdentification;
567                                }
568                        };
569                }
570
571                TransactionStatus status = null;
572                if (txAttr != null) {
573                        if (tm != null) {
574                                status = tm.getTransaction(txAttr);
575                        }
576                        else {
577                                if (logger.isDebugEnabled()) {
578                                        logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
579                                                        "] because no transaction manager has been configured");
580                                }
581                        }
582                }
583                return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
584        }
585
586        /**
587         * Prepare a TransactionInfo for the given attribute and status object.
588         * @param txAttr the TransactionAttribute (may be {@code null})
589         * @param joinpointIdentification the fully qualified method name
590         * (used for monitoring and logging purposes)
591         * @param status the TransactionStatus for the current transaction
592         * @return the prepared TransactionInfo object
593         */
594        protected TransactionInfo prepareTransactionInfo(@Nullable PlatformTransactionManager tm,
595                        @Nullable TransactionAttribute txAttr, String joinpointIdentification,
596                        @Nullable TransactionStatus status) {
597
598                TransactionInfo txInfo = new TransactionInfo(tm, txAttr, joinpointIdentification);
599                if (txAttr != null) {
600                        // We need a transaction for this method...
601                        if (logger.isTraceEnabled()) {
602                                logger.trace("Getting transaction for [" + txInfo.getJoinpointIdentification() + "]");
603                        }
604                        // The transaction manager will flag an error if an incompatible tx already exists.
605                        txInfo.newTransactionStatus(status);
606                }
607                else {
608                        // The TransactionInfo.hasTransaction() method will return false. We created it only
609                        // to preserve the integrity of the ThreadLocal stack maintained in this class.
610                        if (logger.isTraceEnabled()) {
611                                logger.trace("No need to create transaction for [" + joinpointIdentification +
612                                                "]: This method is not transactional.");
613                        }
614                }
615
616                // We always bind the TransactionInfo to the thread, even if we didn't create
617                // a new transaction here. This guarantees that the TransactionInfo stack
618                // will be managed correctly even if no transaction was created by this aspect.
619                txInfo.bindToThread();
620                return txInfo;
621        }
622
623        /**
624         * Execute after successful completion of call, but not after an exception was handled.
625         * Do nothing if we didn't create a transaction.
626         * @param txInfo information about the current transaction
627         */
628        protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
629                if (txInfo != null && txInfo.getTransactionStatus() != null) {
630                        if (logger.isTraceEnabled()) {
631                                logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
632                        }
633                        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
634                }
635        }
636
637        /**
638         * Handle a throwable, completing the transaction.
639         * We may commit or roll back, depending on the configuration.
640         * @param txInfo information about the current transaction
641         * @param ex throwable encountered
642         */
643        protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
644                if (txInfo != null && txInfo.getTransactionStatus() != null) {
645                        if (logger.isTraceEnabled()) {
646                                logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
647                                                "] after exception: " + ex);
648                        }
649                        if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
650                                try {
651                                        txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
652                                }
653                                catch (TransactionSystemException ex2) {
654                                        logger.error("Application exception overridden by rollback exception", ex);
655                                        ex2.initApplicationException(ex);
656                                        throw ex2;
657                                }
658                                catch (RuntimeException | Error ex2) {
659                                        logger.error("Application exception overridden by rollback exception", ex);
660                                        throw ex2;
661                                }
662                        }
663                        else {
664                                // We don't roll back on this exception.
665                                // Will still roll back if TransactionStatus.isRollbackOnly() is true.
666                                try {
667                                        txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
668                                }
669                                catch (TransactionSystemException ex2) {
670                                        logger.error("Application exception overridden by commit exception", ex);
671                                        ex2.initApplicationException(ex);
672                                        throw ex2;
673                                }
674                                catch (RuntimeException | Error ex2) {
675                                        logger.error("Application exception overridden by commit exception", ex);
676                                        throw ex2;
677                                }
678                        }
679                }
680        }
681
682        /**
683         * Reset the TransactionInfo ThreadLocal.
684         * <p>Call this in all cases: exception or normal return!
685         * @param txInfo information about the current transaction (may be {@code null})
686         */
687        protected void cleanupTransactionInfo(@Nullable TransactionInfo txInfo) {
688                if (txInfo != null) {
689                        txInfo.restoreThreadLocalStatus();
690                }
691        }
692
693
694        /**
695         * Opaque object used to hold transaction information. Subclasses
696         * must pass it back to methods on this class, but not see its internals.
697         */
698        protected static final class TransactionInfo {
699
700                @Nullable
701                private final PlatformTransactionManager transactionManager;
702
703                @Nullable
704                private final TransactionAttribute transactionAttribute;
705
706                private final String joinpointIdentification;
707
708                @Nullable
709                private TransactionStatus transactionStatus;
710
711                @Nullable
712                private TransactionInfo oldTransactionInfo;
713
714                public TransactionInfo(@Nullable PlatformTransactionManager transactionManager,
715                                @Nullable TransactionAttribute transactionAttribute, String joinpointIdentification) {
716
717                        this.transactionManager = transactionManager;
718                        this.transactionAttribute = transactionAttribute;
719                        this.joinpointIdentification = joinpointIdentification;
720                }
721
722                public PlatformTransactionManager getTransactionManager() {
723                        Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
724                        return this.transactionManager;
725                }
726
727                @Nullable
728                public TransactionAttribute getTransactionAttribute() {
729                        return this.transactionAttribute;
730                }
731
732                /**
733                 * Return a String representation of this joinpoint (usually a Method call)
734                 * for use in logging.
735                 */
736                public String getJoinpointIdentification() {
737                        return this.joinpointIdentification;
738                }
739
740                public void newTransactionStatus(@Nullable TransactionStatus status) {
741                        this.transactionStatus = status;
742                }
743
744                @Nullable
745                public TransactionStatus getTransactionStatus() {
746                        return this.transactionStatus;
747                }
748
749                /**
750                 * Return whether a transaction was created by this aspect,
751                 * or whether we just have a placeholder to keep ThreadLocal stack integrity.
752                 */
753                public boolean hasTransaction() {
754                        return (this.transactionStatus != null);
755                }
756
757                private void bindToThread() {
758                        // Expose current TransactionStatus, preserving any existing TransactionStatus
759                        // for restoration after this transaction is complete.
760                        this.oldTransactionInfo = transactionInfoHolder.get();
761                        transactionInfoHolder.set(this);
762                }
763
764                private void restoreThreadLocalStatus() {
765                        // Use stack to restore old transaction TransactionInfo.
766                        // Will be null if none was set.
767                        transactionInfoHolder.set(this.oldTransactionInfo);
768                }
769
770                @Override
771                public String toString() {
772                        return (this.transactionAttribute != null ? this.transactionAttribute.toString() : "No transaction");
773                }
774        }
775
776
777        /**
778         * Simple callback interface for proceeding with the target invocation.
779         * Concrete interceptors/aspects adapt this to their invocation mechanism.
780         */
781        @FunctionalInterface
782        protected interface InvocationCallback {
783
784                @Nullable
785                Object proceedWithInvocation() throws Throwable;
786        }
787
788
789        /**
790         * Internal holder class for a Throwable in a callback transaction model.
791         */
792        private static class ThrowableHolder {
793
794                @Nullable
795                public Throwable throwable;
796        }
797
798
799        /**
800         * Internal holder class for a Throwable, used as a RuntimeException to be
801         * thrown from a TransactionCallback (and subsequently unwrapped again).
802         */
803        @SuppressWarnings("serial")
804        private static class ThrowableHolderException extends RuntimeException {
805
806                public ThrowableHolderException(Throwable throwable) {
807                        super(throwable);
808                }
809
810                @Override
811                public String toString() {
812                        return getCause().toString();
813                }
814        }
815
816
817        /**
818         * Inner class to avoid a hard dependency on the Vavr library at runtime.
819         */
820        private static class VavrDelegate {
821
822                public static boolean isVavrTry(Object retVal) {
823                        return (retVal instanceof Try);
824                }
825
826                public static Object evaluateTryFailure(Object retVal, TransactionAttribute txAttr, TransactionStatus status) {
827                        return ((Try<?>) retVal).onFailure(ex -> {
828                                if (txAttr.rollbackOn(ex)) {
829                                        status.setRollbackOnly();
830                                }
831                        });
832                }
833        }
834
835        /**
836         * Inner class to avoid a hard dependency on Kotlin at runtime.
837         */
838        private static class KotlinDelegate {
839
840                static private boolean isSuspend(Method method) {
841                        KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
842                        return function != null && function.isSuspend();
843                }
844        }
845
846
847        /**
848         * Delegate for Reactor-based management of transactional methods with a
849         * reactive return type.
850         */
851        private class ReactiveTransactionSupport {
852
853                private final ReactiveAdapter adapter;
854
855                public ReactiveTransactionSupport(ReactiveAdapter adapter) {
856                        this.adapter = adapter;
857                }
858
859                public Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
860                                InvocationCallback invocation, @Nullable TransactionAttribute txAttr, ReactiveTransactionManager rtm) {
861
862                        String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
863
864                        // Optimize for Mono
865                        if (Mono.class.isAssignableFrom(method.getReturnType())) {
866                                return TransactionContextManager.currentContext().flatMap(context ->
867                                                createTransactionIfNecessary(rtm, txAttr, joinpointIdentification).flatMap(it -> {
868                                                        try {
869                                                                // Need re-wrapping until we get hold of the exception through usingWhen.
870                                                                return Mono.<Object, ReactiveTransactionInfo>usingWhen(
871                                                                                Mono.just(it),
872                                                                                txInfo -> {
873                                                                                        try {
874                                                                                                return (Mono<?>) invocation.proceedWithInvocation();
875                                                                                        }
876                                                                                        catch (Throwable ex) {
877                                                                                                return Mono.error(ex);
878                                                                                        }
879                                                                                },
880                                                                                this::commitTransactionAfterReturning,
881                                                                                (txInfo, err) -> Mono.empty(),
882                                                                                this::commitTransactionAfterReturning)
883                                                                                .onErrorResume(ex ->
884                                                                                                completeTransactionAfterThrowing(it, ex).then(Mono.error(ex)));
885                                                        }
886                                                        catch (Throwable ex) {
887                                                                // target invocation exception
888                                                                return completeTransactionAfterThrowing(it, ex).then(Mono.error(ex));
889                                                        }
890                                                })).subscriberContext(TransactionContextManager.getOrCreateContext())
891                                                .subscriberContext(TransactionContextManager.getOrCreateContextHolder());
892                        }
893
894                        // Any other reactive type, typically a Flux
895                        return this.adapter.fromPublisher(TransactionContextManager.currentContext().flatMapMany(context ->
896                                        createTransactionIfNecessary(rtm, txAttr, joinpointIdentification).flatMapMany(it -> {
897                                                try {
898                                                        // Need re-wrapping until we get hold of the exception through usingWhen.
899                                                        return Flux
900                                                                        .usingWhen(
901                                                                                        Mono.just(it),
902                                                                                        txInfo -> {
903                                                                                                try {
904                                                                                                        return this.adapter.toPublisher(invocation.proceedWithInvocation());
905                                                                                                }
906                                                                                                catch (Throwable ex) {
907                                                                                                        return Mono.error(ex);
908                                                                                                }
909                                                                                        },
910                                                                                        this::commitTransactionAfterReturning,
911                                                                                        (txInfo, ex) -> Mono.empty(),
912                                                                                        this::commitTransactionAfterReturning)
913                                                                        .onErrorResume(ex ->
914                                                                                        completeTransactionAfterThrowing(it, ex).then(Mono.error(ex)));
915                                                }
916                                                catch (Throwable ex) {
917                                                        // target invocation exception
918                                                        return completeTransactionAfterThrowing(it, ex).then(Mono.error(ex));
919                                                }
920                                        })).subscriberContext(TransactionContextManager.getOrCreateContext())
921                                        .subscriberContext(TransactionContextManager.getOrCreateContextHolder()));
922                }
923
924                @SuppressWarnings("serial")
925                private Mono<ReactiveTransactionInfo> createTransactionIfNecessary(ReactiveTransactionManager tm,
926                                @Nullable TransactionAttribute txAttr, final String joinpointIdentification) {
927
928                        // If no name specified, apply method identification as transaction name.
929                        if (txAttr != null && txAttr.getName() == null) {
930                                txAttr = new DelegatingTransactionAttribute(txAttr) {
931                                        @Override
932                                        public String getName() {
933                                                return joinpointIdentification;
934                                        }
935                                };
936                        }
937
938                        final TransactionAttribute attrToUse = txAttr;
939                        Mono<ReactiveTransaction> tx = (attrToUse != null ? tm.getReactiveTransaction(attrToUse) : Mono.empty());
940                        return tx.map(it -> prepareTransactionInfo(tm, attrToUse, joinpointIdentification, it)).switchIfEmpty(
941                                        Mono.defer(() -> Mono.just(prepareTransactionInfo(tm, attrToUse, joinpointIdentification, null))));
942                }
943
944                private ReactiveTransactionInfo prepareTransactionInfo(@Nullable ReactiveTransactionManager tm,
945                                @Nullable TransactionAttribute txAttr, String joinpointIdentification,
946                                @Nullable ReactiveTransaction transaction) {
947
948                        ReactiveTransactionInfo txInfo = new ReactiveTransactionInfo(tm, txAttr, joinpointIdentification);
949                        if (txAttr != null) {
950                                // We need a transaction for this method...
951                                if (logger.isTraceEnabled()) {
952                                        logger.trace("Getting transaction for [" + txInfo.getJoinpointIdentification() + "]");
953                                }
954                                // The transaction manager will flag an error if an incompatible tx already exists.
955                                txInfo.newReactiveTransaction(transaction);
956                        }
957                        else {
958                                // The TransactionInfo.hasTransaction() method will return false. We created it only
959                                // to preserve the integrity of the ThreadLocal stack maintained in this class.
960                                if (logger.isTraceEnabled()) {
961                                        logger.trace("Don't need to create transaction for [" + joinpointIdentification +
962                                                        "]: This method isn't transactional.");
963                                }
964                        }
965
966                        return txInfo;
967                }
968
969                private Mono<Void> commitTransactionAfterReturning(@Nullable ReactiveTransactionInfo txInfo) {
970                        if (txInfo != null && txInfo.getReactiveTransaction() != null) {
971                                if (logger.isTraceEnabled()) {
972                                        logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
973                                }
974                                return txInfo.getTransactionManager().commit(txInfo.getReactiveTransaction());
975                        }
976                        return Mono.empty();
977                }
978
979                private Mono<Void> completeTransactionAfterThrowing(@Nullable ReactiveTransactionInfo txInfo, Throwable ex) {
980                        if (txInfo != null && txInfo.getReactiveTransaction() != null) {
981                                if (logger.isTraceEnabled()) {
982                                        logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
983                                                        "] after exception: " + ex);
984                                }
985                                if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
986                                        return txInfo.getTransactionManager().rollback(txInfo.getReactiveTransaction()).onErrorMap(ex2 -> {
987                                                                logger.error("Application exception overridden by rollback exception", ex);
988                                                                if (ex2 instanceof TransactionSystemException) {
989                                                                        ((TransactionSystemException) ex2).initApplicationException(ex);
990                                                                }
991                                                                return ex2;
992                                                        }
993                                        );
994                                }
995                                else {
996                                        // We don't roll back on this exception.
997                                        // Will still roll back if TransactionStatus.isRollbackOnly() is true.
998                                        return txInfo.getTransactionManager().commit(txInfo.getReactiveTransaction()).onErrorMap(ex2 -> {
999                                                                logger.error("Application exception overridden by commit exception", ex);
1000                                                                if (ex2 instanceof TransactionSystemException) {
1001                                                                        ((TransactionSystemException) ex2).initApplicationException(ex);
1002                                                                }
1003                                                                return ex2;
1004                                                        }
1005                                        );
1006                                }
1007                        }
1008                        return Mono.empty();
1009                }
1010        }
1011
1012
1013        /**
1014         * Opaque object used to hold transaction information for reactive methods.
1015         */
1016        private static final class ReactiveTransactionInfo {
1017
1018                @Nullable
1019                private final ReactiveTransactionManager transactionManager;
1020
1021                @Nullable
1022                private final TransactionAttribute transactionAttribute;
1023
1024                private final String joinpointIdentification;
1025
1026                @Nullable
1027                private ReactiveTransaction reactiveTransaction;
1028
1029                public ReactiveTransactionInfo(@Nullable ReactiveTransactionManager transactionManager,
1030                                @Nullable TransactionAttribute transactionAttribute, String joinpointIdentification) {
1031
1032                        this.transactionManager = transactionManager;
1033                        this.transactionAttribute = transactionAttribute;
1034                        this.joinpointIdentification = joinpointIdentification;
1035                }
1036
1037                public ReactiveTransactionManager getTransactionManager() {
1038                        Assert.state(this.transactionManager != null, "No ReactiveTransactionManager set");
1039                        return this.transactionManager;
1040                }
1041
1042                @Nullable
1043                public TransactionAttribute getTransactionAttribute() {
1044                        return this.transactionAttribute;
1045                }
1046
1047                /**
1048                 * Return a String representation of this joinpoint (usually a Method call)
1049                 * for use in logging.
1050                 */
1051                public String getJoinpointIdentification() {
1052                        return this.joinpointIdentification;
1053                }
1054
1055                public void newReactiveTransaction(@Nullable ReactiveTransaction transaction) {
1056                        this.reactiveTransaction = transaction;
1057                }
1058
1059                @Nullable
1060                public ReactiveTransaction getReactiveTransaction() {
1061                        return this.reactiveTransaction;
1062                }
1063
1064                @Override
1065                public String toString() {
1066                        return (this.transactionAttribute != null ? this.transactionAttribute.toString() : "No transaction");
1067                }
1068        }
1069
1070}