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}