/* * Copyright 2002-2008 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.transaction.support; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.util.Iterator; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.Constants; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.InvalidTimeoutException; import org.springframework.transaction.NestedTransactionNotSupportedException; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.TransactionSuspensionNotSupportedException; import org.springframework.transaction.UnexpectedRollbackException; /** * Abstract base class that implements Spring's standard transaction workflow, * serving as basis for concrete platform transaction managers like * {@link org.springframework.transaction.jta.JtaTransactionManager} and * {@link org.springframework.jdbc.datasource.DataSourceTransactionManager}. * * <p>This base class provides the following workflow handling: * <ul> * <li>determines if there is an existing transaction; * <li>applies the appropriate propagation behavior; * <li>suspends and resumes transactions if necessary; * <li>checks the rollback-only flag on commit; * <li>applies the appropriate modification on rollback * (actual rollback or setting rollback-only); * <li>triggers registered synchronization callbacks * (if transaction synchronization is active). * </ul> * * <p>Subclasses have to implement specific template methods for specific * states of a transaction, e.g.: begin, suspend, resume, commit, rollback. * The most important of them are abstract and must be provided by a concrete * implementation; for the rest, defaults are provided, so overriding is optional. * * <p>Transaction synchronization is a generic mechanism for registering callbacks * that get invoked at transaction completion time. This is mainly used internally * by the data access support classes for JDBC, Hibernate, JDO, etc when running * within a JTA transaction: They register resources that are opened within the * transaction for closing at transaction completion time, allowing e.g. for reuse * of the same Hibernate Session within the transaction. The same mechanism can * also be leveraged for custom synchronization needs in an application. * * <p>The state of this class is serializable, to allow for serializing the * transaction strategy along with proxies that carry a transaction interceptor. * It is up to subclasses if they wish to make their state to be serializable too. * They should implement the <code>java.io.Serializable</code> marker interface in * that case, and potentially a private <code>readObject()</code> method (according * to Java serialization rules) if they need to restore any transient state. * * @author Juergen Hoeller * @since 28.03.2003 * @see #setTransactionSynchronization * @see TransactionSynchronizationManager * @see org.springframework.transaction.jta.JtaTransactionManager * @see org.springframework.jdbc.datasource.DataSourceTransactionManager * @see org.springframework.orm.hibernate3.HibernateTransactionManager */ public abstract class AbstractPlatformTransactionManager implements PlatformTransactionManager, Serializable { /** * Always activate transaction synchronization, even for "empty" transactions * that result from PROPAGATION_SUPPORTS with no existing backend transaction. * @see org.springframework.transaction.TransactionDefinition#PROPAGATION_SUPPORTS * @see org.springframework.transaction.TransactionDefinition#PROPAGATION_NOT_SUPPORTED * @see org.springframework.transaction.TransactionDefinition#PROPAGATION_NEVER */ public static final int SYNCHRONIZATION_ALWAYS = 0; /** * Activate transaction synchronization only for actual transactions, * that is, not for empty ones that result from PROPAGATION_SUPPORTS with * no existing backend transaction. * @see org.springframework.transaction.TransactionDefinition#PROPAGATION_REQUIRED * @see org.springframework.transaction.TransactionDefinition#PROPAGATION_MANDATORY * @see org.springframework.transaction.TransactionDefinition#PROPAGATION_REQUIRES_NEW */ public static final int SYNCHRONIZATION_ON_ACTUAL_TRANSACTION = 1; /** * Never active transaction synchronization, not even for actual transactions. */ public static final int SYNCHRONIZATION_NEVER = 2; /** Constants instance for AbstractPlatformTransactionManager */ private static final Constants constants = new Constants(AbstractPlatformTransactionManager.class); /** Transient to optimize serialization */ protected transient Log logger = LogFactory.getLog(getClass()); private int transactionSynchronization = SYNCHRONIZATION_ALWAYS; private int defaultTimeout = TransactionDefinition.TIMEOUT_DEFAULT; private boolean nestedTransactionAllowed = false; private boolean validateExistingTransaction = false; private boolean globalRollbackOnParticipationFailure = true; private boolean failEarlyOnGlobalRollbackOnly = false; private boolean rollbackOnCommitFailure = false; /** * Set the transaction synchronization by the name of the corresponding constant * in this class, e.g. "SYNCHRONIZATION_ALWAYS". * @param constantName name of the constant * @see #SYNCHRONIZATION_ALWAYS */ public final void setTransactionSynchronizationName(String constantName) { setTransactionSynchronization(constants.asNumber(constantName).intValue()); } /** * Set when this transaction manager should activate the thread-bound * transaction synchronization support. Default is "always". * <p>Note that transaction synchronization isn't supported for * multiple concurrent transactions by different transaction managers. * Only one transaction manager is allowed to activate it at any time. * @see #SYNCHRONIZATION_ALWAYS * @see #SYNCHRONIZATION_ON_ACTUAL_TRANSACTION * @see #SYNCHRONIZATION_NEVER * @see TransactionSynchronizationManager * @see TransactionSynchronization */ public final void setTransactionSynchronization(int transactionSynchronization) { this.transactionSynchronization = transactionSynchronization; } /** * Return if this transaction manager should activate the thread-bound * transaction synchronization support. */ public final int getTransactionSynchronization() { return this.transactionSynchronization; } /** * Specify the default timeout that this transaction manager should apply * if there is no timeout specified at the transaction level, in seconds. * <p>Default is the underlying transaction infrastructure's default timeout, * e.g. typically 30 seconds in case of a JTA provider, indicated by the * <code>TransactionDefinition.TIMEOUT_DEFAULT</code> value. * @see org.springframework.transaction.TransactionDefinition#TIMEOUT_DEFAULT */ public final void setDefaultTimeout(int defaultTimeout) { if (defaultTimeout < TransactionDefinition.TIMEOUT_DEFAULT) { throw new InvalidTimeoutException("Invalid default timeout", defaultTimeout); } this.defaultTimeout = defaultTimeout; } /** * Return the default timeout that this transaction manager should apply * if there is no timeout specified at the transaction level, in seconds. * <p>Returns <code>TransactionDefinition.TIMEOUT_DEFAULT</code> to indicate * the underlying transaction infrastructure's default timeout. */ public final int getDefaultTimeout() { return this.defaultTimeout; } /** * Set whether nested transactions are allowed. Default is "false". * <p>Typically initialized with an appropriate default by the * concrete transaction manager subclass. */ public final void setNestedTransactionAllowed(boolean nestedTransactionAllowed) { this.nestedTransactionAllowed = nestedTransactionAllowed; } /** * Return whether nested transactions are allowed. */ public final boolean isNestedTransactionAllowed() { return this.nestedTransactionAllowed; } /** * Set whether existing transactions should be validated before participating * in them. * <p>When participating in an existing transaction (e.g. with * PROPAGATION_REQUIRES or PROPAGATION_SUPPORTS encountering an existing * transaction), this outer transaction's characteristics will apply even * to the inner transaction scope. Validation will detect incompatible * isolation level and read-only settings on the inner transaction definition * and reject participation accordingly through throwing a corresponding exception. * <p>Default is "false", leniently ignoring inner transaction settings, * simply overriding them with the outer transaction's characteristics. * Switch this flag to "true" in order to enforce strict validation. */ public final void setValidateExistingTransaction(boolean validateExistingTransaction) { this.validateExistingTransaction = validateExistingTransaction; } /** * Return whether existing transactions should be validated before participating * in them. */ public final boolean isValidateExistingTransaction() { return this.validateExistingTransaction; } /** * Set whether to globally mark an existing transaction as rollback-only * after a participating transaction failed. * <p>Default is "true": If a participating transaction (e.g. with * PROPAGATION_REQUIRES or PROPAGATION_SUPPORTS encountering an existing * transaction) fails, the transaction will be globally marked as rollback-only. * The only possible outcome of such a transaction is a rollback: The * transaction originator <i>cannot</i> make the transaction commit anymore. * <p>Switch this to "false" to let the transaction originator make the rollback * decision. If a participating transaction fails with an exception, the caller * can still decide to continue with a different path within the transaction. * However, note that this will only work as long as all participating resources * are capable of continuing towards a transaction commit even after a data access * failure: This is generally not the case for a Hibernate Session, for example; * neither is it for a sequence of JDBC insert/update/delete operations. * <p><b>Note:</b>This flag only applies to an explicit rollback attempt for a * subtransaction, typically caused by an exception thrown by a data access operation * (where TransactionInterceptor will trigger a <code>PlatformTransactionManager.rollback()</code> * call according to a rollback rule). If the flag is off, the caller can handle the exception * and decide on a rollback, independent of the rollback rules of the subtransaction. * This flag does, however, <i>not</i> apply to explicit <code>setRollbackOnly</code> * calls on a <code>TransactionStatus</code>, which will always cause an eventual * global rollback (as it might not throw an exception after the rollback-only call). * <p>The recommended solution for handling failure of a subtransaction * is a "nested transaction", where the global transaction can be rolled * back to a savepoint taken at the beginning of the subtransaction. * PROPAGATION_NESTED provides exactly those semantics; however, it will * only work when nested transaction support is available. This is the case * with DataSourceTransactionManager, but not with JtaTransactionManager. * @see #setNestedTransactionAllowed * @see org.springframework.jdbc.datasource.DataSourceTransactionManager * @see org.springframework.transaction.jta.JtaTransactionManager */ public final void setGlobalRollbackOnParticipationFailure(boolean globalRollbackOnParticipationFailure) { this.globalRollbackOnParticipationFailure = globalRollbackOnParticipationFailure; } /** * Return whether to globally mark an existing transaction as rollback-only * after a participating transaction failed. */ public final boolean isGlobalRollbackOnParticipationFailure() { return this.globalRollbackOnParticipationFailure; } /** * Set whether to fail early in case of the transaction being globally marked * as rollback-only. * <p>Default is "false", only causing an UnexpectedRollbackException at the * outermost transaction boundary. Switch this flag on to cause an * UnexpectedRollbackException as early as the global rollback-only marker * has been first detected, even from within an inner transaction boundary. * <p>Note that, as of Spring 2.0, the fail-early behavior for global * rollback-only markers has been unified: All transaction managers will by * default only cause UnexpectedRollbackException at the outermost transaction * boundary. This allows, for example, to continue unit tests even after an * operation failed and the transaction will never be completed. All transaction * managers will only fail earlier if this flag has explicitly been set to "true". * @see org.springframework.transaction.UnexpectedRollbackException */ public final void setFailEarlyOnGlobalRollbackOnly(boolean failEarlyOnGlobalRollbackOnly) { this.failEarlyOnGlobalRollbackOnly = failEarlyOnGlobalRollbackOnly; } /** * Return whether to fail early in case of the transaction being globally marked * as rollback-only. */ public final boolean isFailEarlyOnGlobalRollbackOnly() { return this.failEarlyOnGlobalRollbackOnly; } /** * Set whether <code>doRollback</code> should be performed on failure of the * <code>doCommit</code> call. Typically not necessary and thus to be avoided, * as it can potentially override the commit exception with a subsequent * rollback exception. * <p>Default is "false". * @see #doCommit * @see #doRollback */ public final void setRollbackOnCommitFailure(boolean rollbackOnCommitFailure) { this.rollbackOnCommitFailure = rollbackOnCommitFailure; } /** * Return whether <code>doRollback</code> should be performed on failure of the * <code>doCommit</code> call. */ public final boolean isRollbackOnCommitFailure() { return this.rollbackOnCommitFailure; } //--------------------------------------------------------------------- // Implementation of PlatformTransactionManager //--------------------------------------------------------------------- /** * This implementation handles propagation behavior. Delegates to * <code>doGetTransaction</code>, <code>isExistingTransaction</code> * and <code>doBegin</code>. * @see #doGetTransaction * @see #isExistingTransaction * @see #doBegin */ public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException { Object transaction = doGetTransaction(); // Cache debug flag to avoid repeated checks. boolean debugEnabled = logger.isDebugEnabled(); if (definition == null) { // Use defaults if no transaction definition given. definition = new DefaultTransactionDefinition(); } if (isExistingTransaction(transaction)) { // Existing transaction found -> check propagation behavior to find out how to behave. return handleExistingTransaction(definition, transaction, debugEnabled); } // Check definition settings for new transaction. if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout()); } // No existing transaction found -> check propagation behavior to find out how to proceed. if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { throw new IllegalTransactionStateException( "No existing transaction found for transaction marked with propagation 'mandatory'"); } else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { SuspendedResourcesHolder suspendedResources = suspend(null); if (debugEnabled) { logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); } try { doBegin(transaction, definition); } catch (RuntimeException ex) { resume(null, suspendedResources); throw ex; } catch (Error err) { resume(null, suspendedResources); throw err; } boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); return newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); } else { // Create "empty" transaction: no actual transaction, but potentially synchronization. boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); return newTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); } } /** * Create a TransactionStatus for an existing transaction. */ private TransactionStatus handleExistingTransaction( TransactionDefinition definition, Object transaction, boolean debugEnabled) throws TransactionException { if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { throw new IllegalTransactionStateException( "Existing transaction found for transaction marked with propagation 'never'"); } if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { if (debugEnabled) { logger.debug("Suspending current transaction"); } Object suspendedResources = suspend(transaction); boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); return newTransactionStatus( definition, null, false, newSynchronization, debugEnabled, suspendedResources); } if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { if (debugEnabled) { logger.debug("Suspending current transaction, creating new transaction with name [" + definition.getName() + "]"); } SuspendedResourcesHolder suspendedResources = suspend(transaction); try { doBegin(transaction, definition); } catch (RuntimeException beginEx) { resumeAfterBeginException(transaction, suspendedResources, beginEx); throw beginEx; } catch (Error beginErr) { resumeAfterBeginException(transaction, suspendedResources, beginErr); throw beginErr; } boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); return newTransactionStatus( definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); } if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { if (!isNestedTransactionAllowed()) { throw new NestedTransactionNotSupportedException( "Transaction manager does not allow nested transactions by default - " + "specify 'nestedTransactionAllowed' property with value 'true'"); } if (debugEnabled) { logger.debug("Creating nested transaction with name [" + definition.getName() + "]"); } if (useSavepointForNestedTransaction()) { // Create savepoint within existing Spring-managed transaction, // through the SavepointManager API implemented by TransactionStatus. // Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization. DefaultTransactionStatus status = newTransactionStatus(definition, transaction, false, false, debugEnabled, null); status.createAndHoldSavepoint(); return status; } else { // Nested transaction through nested begin and commit/rollback calls. // Usually only for JTA: Spring synchronization might get activated here // in case of a pre-existing JTA transaction. doBegin(transaction, definition); boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); return newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, null); } } // Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED. if (debugEnabled) { logger.debug("Participating in existing transaction"); } if (isValidateExistingTransaction()) { if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { Integer currentIsolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); if (currentIsolationLevel == null || currentIsolationLevel.intValue() != definition.getIsolationLevel()) { Constants isoConstants = DefaultTransactionDefinition.constants; throw new IllegalTransactionStateException("Participating transaction with definition [" + definition + "] specifies isolation level which is incompatible with existing transaction: " + (currentIsolationLevel != null ? isoConstants.toCode(currentIsolationLevel, DefaultTransactionDefinition.PREFIX_ISOLATION) : "(unknown)")); } } if (!definition.isReadOnly()) { if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { throw new IllegalTransactionStateException("Participating transaction with definition [" + definition + "] is not marked as read-only but existing transaction is"); } } } boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); return newTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); } /** * Create a new TransactionStatus for the given arguments, * initializing transaction synchronization as appropriate. */ protected DefaultTransactionStatus newTransactionStatus( TransactionDefinition definition, Object transaction, boolean newTransaction, boolean newSynchronization, boolean debug, Object suspendedResources) { boolean actualNewSynchronization = newSynchronization && !TransactionSynchronizationManager.isSynchronizationActive(); if (actualNewSynchronization) { TransactionSynchronizationManager.setActualTransactionActive(transaction != null); TransactionSynchronizationManager.setCurrentTransactionIsolationLevel( (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) ? new Integer(definition.getIsolationLevel()) : null); TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly()); TransactionSynchronizationManager.setCurrentTransactionName(definition.getName()); TransactionSynchronizationManager.initSynchronization(); } return new DefaultTransactionStatus( transaction, newTransaction, actualNewSynchronization, definition.isReadOnly(), debug, suspendedResources); } /** * Determine the actual timeout to use for the given definition. * Will fall back to this manager's default timeout if the * transaction definition doesn't specify a non-default value. * @param definition the transaction definition * @return the actual timeout to use * @see org.springframework.transaction.TransactionDefinition#getTimeout() * @see #setDefaultTimeout */ protected int determineTimeout(TransactionDefinition definition) { if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) { return definition.getTimeout(); } return this.defaultTimeout; } /** * Suspend the given transaction. Suspends transaction synchronization first, * then delegates to the <code>doSuspend</code> template method. * @param transaction the current transaction object * (or <code>null</code> to just suspend active synchronizations, if any) * @return an object that holds suspended resources * (or <code>null</code> if neither transaction nor synchronization active) * @see #doSuspend * @see #resume */ protected final SuspendedResourcesHolder suspend(Object transaction) throws TransactionException { if (TransactionSynchronizationManager.isSynchronizationActive()) { List suspendedSynchronizations = doSuspendSynchronization(); try { Object suspendedResources = null; if (transaction != null) { suspendedResources = doSuspend(transaction); } String name = TransactionSynchronizationManager.getCurrentTransactionName(); TransactionSynchronizationManager.setCurrentTransactionName(null); boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); TransactionSynchronizationManager.setCurrentTransactionReadOnly(false); Integer isolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null); boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive(); TransactionSynchronizationManager.setActualTransactionActive(false); return new SuspendedResourcesHolder( suspendedResources, suspendedSynchronizations, name, readOnly, isolationLevel, wasActive); } catch (RuntimeException ex) { // doSuspend failed - original transaction is still active... doResumeSynchronization(suspendedSynchronizations); throw ex; } catch (Error err) { // doSuspend failed - original transaction is still active... doResumeSynchronization(suspendedSynchronizations); throw err; } } else if (transaction != null) { // Transaction active but no synchronization active. Object suspendedResources = doSuspend(transaction); return new SuspendedResourcesHolder(suspendedResources); } else { // Neither transaction nor synchronization active. return null; } } /** * Resume the given transaction. Delegates to the <code>doResume</code> * template method first, then resuming transaction synchronization. * @param transaction the current transaction object * @param resourcesHolder the object that holds suspended resources, * as returned by <code>suspend</code> (or <code>null</code> to just * resume synchronizations, if any) * @see #doResume * @see #suspend */ protected final void resume(Object transaction, SuspendedResourcesHolder resourcesHolder) throws TransactionException { if (resourcesHolder != null) { Object suspendedResources = resourcesHolder.suspendedResources; if (suspendedResources != null) { doResume(transaction, suspendedResources); } List suspendedSynchronizations = resourcesHolder.suspendedSynchronizations; if (suspendedSynchronizations != null) { TransactionSynchronizationManager.setActualTransactionActive(resourcesHolder.wasActive); TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(resourcesHolder.isolationLevel); TransactionSynchronizationManager.setCurrentTransactionReadOnly(resourcesHolder.readOnly); TransactionSynchronizationManager.setCurrentTransactionName(resourcesHolder.name); doResumeSynchronization(suspendedSynchronizations); } } } /** * Resume outer transaction after inner transaction begin failed. */ private void resumeAfterBeginException( Object transaction, SuspendedResourcesHolder suspendedResources, Throwable beginEx) { String exMessage = "Inner transaction begin exception overridden by outer transaction resume exception"; try { resume(transaction, suspendedResources); } catch (RuntimeException resumeEx) { logger.error(exMessage, beginEx); throw resumeEx; } catch (Error resumeErr) { logger.error(exMessage, beginEx); throw resumeErr; } } /** * Suspend all current synchronizations and deactivate transaction * synchronization for the current thread. * @return the List of suspended TransactionSynchronization objects */ private List doSuspendSynchronization() { List suspendedSynchronizations = TransactionSynchronizationManager.getSynchronizations(); for (Iterator it = suspendedSynchronizations.iterator(); it.hasNext();) { ((TransactionSynchronization) it.next()).suspend(); } TransactionSynchronizationManager.clearSynchronization(); return suspendedSynchronizations; } /** * Reactivate transaction synchronization for the current thread * and resume all given synchronizations. * @param suspendedSynchronizations List of TransactionSynchronization objects */ private void doResumeSynchronization(List suspendedSynchronizations) { TransactionSynchronizationManager.initSynchronization(); for (Iterator it = suspendedSynchronizations.iterator(); it.hasNext();) { TransactionSynchronization synchronization = (TransactionSynchronization) it.next(); synchronization.resume(); TransactionSynchronizationManager.registerSynchronization(synchronization); } } /** * This implementation of commit handles participating in existing * transactions and programmatic rollback requests. * Delegates to <code>isRollbackOnly</code>, <code>doCommit</code> * and <code>rollback</code>. * @see org.springframework.transaction.TransactionStatus#isRollbackOnly() * @see #doCommit * @see #rollback */ public final void commit(TransactionStatus status) throws TransactionException { if (status.isCompleted()) { throw new IllegalTransactionStateException( "Transaction is already completed - do not call commit or rollback more than once per transaction"); } DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; if (defStatus.isLocalRollbackOnly()) { if (defStatus.isDebug()) { logger.debug("Transactional code has requested rollback"); } processRollback(defStatus); return; } if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) { if (defStatus.isDebug()) { logger.debug("Global transaction is marked as rollback-only but transactional code requested commit"); } processRollback(defStatus); // Throw UnexpectedRollbackException only at outermost transaction boundary // or if explicitly asked to. if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) { throw new UnexpectedRollbackException( "Transaction rolled back because it has been marked as rollback-only"); } return; } processCommit(defStatus); } /** * Process an actual commit. * Rollback-only flags have already been checked and applied. * @param status object representing the transaction * @throws TransactionException in case of commit failure */ private void processCommit(DefaultTransactionStatus status) throws TransactionException { try { boolean beforeCompletionInvoked = false; try { prepareForCommit(status); triggerBeforeCommit(status); triggerBeforeCompletion(status); beforeCompletionInvoked = true; boolean globalRollbackOnly = false; if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) { globalRollbackOnly = status.isGlobalRollbackOnly(); } if (status.hasSavepoint()) { if (status.isDebug()) { logger.debug("Releasing transaction savepoint"); } status.releaseHeldSavepoint(); } else if (status.isNewTransaction()) { if (status.isDebug()) { logger.debug("Initiating transaction commit"); } doCommit(status); } // Throw UnexpectedRollbackException if we have a global rollback-only // marker but still didn't get a corresponding exception from commit. if (globalRollbackOnly) { throw new UnexpectedRollbackException( "Transaction silently rolled back because it has been marked as rollback-only"); } } catch (UnexpectedRollbackException ex) { // can only be caused by doCommit triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); throw ex; } catch (TransactionException ex) { // can only be caused by doCommit if (isRollbackOnCommitFailure()) { doRollbackOnCommitException(status, ex); } else { triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); } throw ex; } catch (RuntimeException ex) { if (!beforeCompletionInvoked) { triggerBeforeCompletion(status); } doRollbackOnCommitException(status, ex); throw ex; } catch (Error err) { if (!beforeCompletionInvoked) { triggerBeforeCompletion(status); } doRollbackOnCommitException(status, err); throw err; } // Trigger afterCommit callbacks, with an exception thrown there // propagated to callers but the transaction still considered as committed. try { triggerAfterCommit(status); } finally { triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); } } finally { cleanupAfterCompletion(status); } } /** * This implementation of rollback handles participating in existing * transactions. Delegates to <code>doRollback</code> and * <code>doSetRollbackOnly</code>. * @see #doRollback * @see #doSetRollbackOnly */ public final void rollback(TransactionStatus status) throws TransactionException { if (status.isCompleted()) { throw new IllegalTransactionStateException( "Transaction is already completed - do not call commit or rollback more than once per transaction"); } DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; processRollback(defStatus); } /** * Process an actual rollback. * The completed flag has already been checked. * @param status object representing the transaction * @throws TransactionException in case of rollback failure */ private void processRollback(DefaultTransactionStatus status) { try { try { triggerBeforeCompletion(status); if (status.hasSavepoint()) { if (status.isDebug()) { logger.debug("Rolling back transaction to savepoint"); } status.rollbackToHeldSavepoint(); } else if (status.isNewTransaction()) { if (status.isDebug()) { logger.debug("Initiating transaction rollback"); } doRollback(status); } else if (status.hasTransaction()) { if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) { if (status.isDebug()) { logger.debug( "Participating transaction failed - marking existing transaction as rollback-only"); } doSetRollbackOnly(status); } else { if (status.isDebug()) { logger.debug( "Participating transaction failed - letting transaction originator decide on rollback"); } } } else { logger.debug("Should roll back transaction but cannot - no transaction available"); } } catch (RuntimeException ex) { triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); throw ex; } catch (Error err) { triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); throw err; } triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); } finally { cleanupAfterCompletion(status); } } /** * Invoke <code>doRollback</code>, handling rollback exceptions properly. * @param status object representing the transaction * @param ex the thrown application exception or error * @throws TransactionException in case of rollback failure * @see #doRollback */ private void doRollbackOnCommitException(DefaultTransactionStatus status, Throwable ex) throws TransactionException { try { if (status.isNewTransaction()) { if (status.isDebug()) { logger.debug("Initiating transaction rollback after commit exception", ex); } doRollback(status); } else if (status.hasTransaction() && isGlobalRollbackOnParticipationFailure()) { if (status.isDebug()) { logger.debug("Marking existing transaction as rollback-only after commit exception", ex); } doSetRollbackOnly(status); } } catch (RuntimeException rbex) { logger.error("Commit exception overridden by rollback exception", ex); triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); throw rbex; } catch (Error rberr) { logger.error("Commit exception overridden by rollback exception", ex); triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); throw rberr; } triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); } /** * Trigger <code>beforeCommit</code> callbacks. * @param status object representing the transaction */ protected final void triggerBeforeCommit(DefaultTransactionStatus status) { if (status.isNewSynchronization()) { if (status.isDebug()) { logger.trace("Triggering beforeCommit synchronization"); } TransactionSynchronizationUtils.triggerBeforeCommit(status.isReadOnly()); } } /** * Trigger <code>beforeCompletion</code> callbacks. * @param status object representing the transaction */ protected final void triggerBeforeCompletion(DefaultTransactionStatus status) { if (status.isNewSynchronization()) { if (status.isDebug()) { logger.trace("Triggering beforeCompletion synchronization"); } TransactionSynchronizationUtils.triggerBeforeCompletion(); } } /** * Trigger <code>afterCommit</code> callbacks. * @param status object representing the transaction */ private void triggerAfterCommit(DefaultTransactionStatus status) { if (status.isNewSynchronization()) { if (status.isDebug()) { logger.trace("Triggering afterCommit synchronization"); } TransactionSynchronizationUtils.triggerAfterCommit(); } } /** * Trigger <code>afterCompletion</code> callbacks. * @param status object representing the transaction * @param completionStatus completion status according to TransactionSynchronization constants */ private void triggerAfterCompletion(DefaultTransactionStatus status, int completionStatus) { if (status.isNewSynchronization()) { List synchronizations = TransactionSynchronizationManager.getSynchronizations(); if (!status.hasTransaction() || status.isNewTransaction()) { if (status.isDebug()) { logger.trace("Triggering afterCompletion synchronization"); } // No transaction or new transaction for the current scope -> // invoke the afterCompletion callbacks immediately invokeAfterCompletion(synchronizations, completionStatus); } else { // Existing transaction that we participate in, controlled outside // of the scope of this Spring transaction manager -> try to register // an afterCompletion callback with the existing (JTA) transaction. registerAfterCompletionWithExistingTransaction(status.getTransaction(), synchronizations); } } } /** * Actually invoke the <code>afterCompletion</code> methods of the * given Spring TransactionSynchronization objects. * <p>To be called by this abstract manager itself, or by special implementations * of the <code>registerAfterCompletionWithExistingTransaction</code> callback. * @param synchronizations List of TransactionSynchronization objects * @param completionStatus the completion status according to the * constants in the TransactionSynchronization interface * @see #registerAfterCompletionWithExistingTransaction(Object, java.util.List) * @see TransactionSynchronization#STATUS_COMMITTED * @see TransactionSynchronization#STATUS_ROLLED_BACK * @see TransactionSynchronization#STATUS_UNKNOWN */ protected final void invokeAfterCompletion(List synchronizations, int completionStatus) { TransactionSynchronizationUtils.invokeAfterCompletion(synchronizations, completionStatus); } /** * Clean up after completion, clearing synchronization if necessary, * and invoking doCleanupAfterCompletion. * @param status object representing the transaction * @see #doCleanupAfterCompletion */ private void cleanupAfterCompletion(DefaultTransactionStatus status) { status.setCompleted(); if (status.isNewSynchronization()) { TransactionSynchronizationManager.clear(); } if (status.isNewTransaction()) { doCleanupAfterCompletion(status.getTransaction()); } if (status.getSuspendedResources() != null) { if (status.isDebug()) { logger.debug("Resuming suspended transaction"); } resume(status.getTransaction(), (SuspendedResourcesHolder) status.getSuspendedResources()); } } //--------------------------------------------------------------------- // Template methods to be implemented in subclasses //--------------------------------------------------------------------- /** * Return a transaction object for the current transaction state. * <p>The returned object will usually be specific to the concrete transaction * manager implementation, carrying corresponding transaction state in a * modifiable fashion. This object will be passed into the other template * methods (e.g. doBegin and doCommit), either directly or as part of a * DefaultTransactionStatus instance. * <p>The returned object should contain information about any existing * transaction, that is, a transaction that has already started before the * current <code>getTransaction</code> call on the transaction manager. * Consequently, a <code>doGetTransaction</code> implementation will usually * look for an existing transaction and store corresponding state in the * returned transaction object. * @return the current transaction object * @throws org.springframework.transaction.CannotCreateTransactionException * if transaction support is not available * @throws TransactionException in case of lookup or system errors * @see #doBegin * @see #doCommit * @see #doRollback * @see DefaultTransactionStatus#getTransaction */ protected abstract Object doGetTransaction() throws TransactionException; /** * Check if the given transaction object indicates an existing transaction * (that is, a transaction which has already started). * <p>The result will be evaluated according to the specified propagation * behavior for the new transaction. An existing transaction might get * suspended (in case of PROPAGATION_REQUIRES_NEW), or the new transaction * might participate in the existing one (in case of PROPAGATION_REQUIRED). * <p>The default implementation returns <code>false</code>, assuming that * participating in existing transactions is generally not supported. * Subclasses are of course encouraged to provide such support. * @param transaction transaction object returned by doGetTransaction * @return if there is an existing transaction * @throws TransactionException in case of system errors * @see #doGetTransaction */ protected boolean isExistingTransaction(Object transaction) throws TransactionException { return false; } /** * Return whether to use a savepoint for a nested transaction. * <p>Default is <code>true</code>, which causes delegation to DefaultTransactionStatus * for creating and holding a savepoint. If the transaction object does not implement * the SavepointManager interface, a NestedTransactionNotSupportedException will be * thrown. Else, the SavepointManager will be asked to create a new savepoint to * demarcate the start of the nested transaction. * <p>Subclasses can override this to return <code>false</code>, causing a further * call to <code>doBegin</code> - within the context of an already existing transaction. * The <code>doBegin</code> implementation needs to handle this accordingly in such * a scenario. This is appropriate for JTA, for example. * @see DefaultTransactionStatus#createAndHoldSavepoint * @see DefaultTransactionStatus#rollbackToHeldSavepoint * @see DefaultTransactionStatus#releaseHeldSavepoint * @see #doBegin */ protected boolean useSavepointForNestedTransaction() { return true; } /** * Begin a new transaction with semantics according to the given transaction * definition. Does not have to care about applying the propagation behavior, * as this has already been handled by this abstract manager. * <p>This method gets called when the transaction manager has decided to actually * start a new transaction. Either there wasn't any transaction before, or the * previous transaction has been suspended. * <p>A special scenario is a nested transaction without savepoint: If * <code>useSavepointForNestedTransaction()</code> returns "false", this method * will be called to start a nested transaction when necessary. In such a context, * there will be an active transaction: The implementation of this method has * to detect this and start an appropriate nested transaction. * @param transaction transaction object returned by <code>doGetTransaction</code> * @param definition TransactionDefinition instance, describing propagation * behavior, isolation level, read-only flag, timeout, and transaction name * @throws TransactionException in case of creation or system errors */ protected abstract void doBegin(Object transaction, TransactionDefinition definition) throws TransactionException; /** * Suspend the resources of the current transaction. * Transaction synchronization will already have been suspended. * <p>The default implementation throws a TransactionSuspensionNotSupportedException, * assuming that transaction suspension is generally not supported. * @param transaction transaction object returned by <code>doGetTransaction</code> * @return an object that holds suspended resources * (will be kept unexamined for passing it into doResume) * @throws org.springframework.transaction.TransactionSuspensionNotSupportedException * if suspending is not supported by the transaction manager implementation * @throws TransactionException in case of system errors * @see #doResume */ protected Object doSuspend(Object transaction) throws TransactionException { throw new TransactionSuspensionNotSupportedException( "Transaction manager [" + getClass().getName() + "] does not support transaction suspension"); } /** * Resume the resources of the current transaction. * Transaction synchronization will be resumed afterwards. * <p>The default implementation throws a TransactionSuspensionNotSupportedException, * assuming that transaction suspension is generally not supported. * @param transaction transaction object returned by <code>doGetTransaction</code> * @param suspendedResources the object that holds suspended resources, * as returned by doSuspend * @throws org.springframework.transaction.TransactionSuspensionNotSupportedException * if resuming is not supported by the transaction manager implementation * @throws TransactionException in case of system errors * @see #doSuspend */ protected void doResume(Object transaction, Object suspendedResources) throws TransactionException { throw new TransactionSuspensionNotSupportedException( "Transaction manager [" + getClass().getName() + "] does not support transaction suspension"); } /** * Return whether to call <code>doCommit</code> on a transaction that has been * marked as rollback-only in a global fashion. * <p>Does not apply if an application locally sets the transaction to rollback-only * via the TransactionStatus, but only to the transaction itself being marked as * rollback-only by the transaction coordinator. * <p>Default is "false": Local transaction strategies usually don't hold the rollback-only * marker in the transaction itself, therefore they can't handle rollback-only transactions * as part of transaction commit. Hence, AbstractPlatformTransactionManager will trigger * a rollback in that case, throwing an UnexpectedRollbackException afterwards. * <p>Override this to return "true" if the concrete transaction manager expects a * <code>doCommit</code> call even for a rollback-only transaction, allowing for * special handling there. This will, for example, be the case for JTA, where * <code>UserTransaction.commit</code> will check the read-only flag itself and * throw a corresponding RollbackException, which might include the specific reason * (such as a transaction timeout). * <p>If this method returns "true" but the <code>doCommit</code> implementation does not * throw an exception, this transaction manager will throw an UnexpectedRollbackException * itself. This should not be the typical case; it is mainly checked to cover misbehaving * JTA providers that silently roll back even when the rollback has not been requested * by the calling code. * @see #doCommit * @see DefaultTransactionStatus#isGlobalRollbackOnly() * @see DefaultTransactionStatus#isLocalRollbackOnly() * @see org.springframework.transaction.TransactionStatus#setRollbackOnly() * @see org.springframework.transaction.UnexpectedRollbackException * @see javax.transaction.UserTransaction#commit() * @see javax.transaction.RollbackException */ protected boolean shouldCommitOnGlobalRollbackOnly() { return false; } /** * Make preparations for commit, to be performed before the * <code>beforeCommit</code> synchronization callbacks occur. * <p>Note that exceptions will get propagated to the commit caller * and cause a rollback of the transaction. * @param status the status representation of the transaction * @throws RuntimeException in case of errors; will be <b>propagated to the caller</b> * (note: do not throw TransactionException subclasses here!) */ protected void prepareForCommit(DefaultTransactionStatus status) { } /** * Perform an actual commit of the given transaction. * <p>An implementation does not need to check the "new transaction" flag * or the rollback-only flag; this will already have been handled before. * Usually, a straight commit will be performed on the transaction object * contained in the passed-in status. * @param status the status representation of the transaction * @throws TransactionException in case of commit or system errors * @see DefaultTransactionStatus#getTransaction */ protected abstract void doCommit(DefaultTransactionStatus status) throws TransactionException; /** * Perform an actual rollback of the given transaction. * <p>An implementation does not need to check the "new transaction" flag; * this will already have been handled before. Usually, a straight rollback * will be performed on the transaction object contained in the passed-in status. * @param status the status representation of the transaction * @throws TransactionException in case of system errors * @see DefaultTransactionStatus#getTransaction */ protected abstract void doRollback(DefaultTransactionStatus status) throws TransactionException; /** * Set the given transaction rollback-only. Only called on rollback * if the current transaction participates in an existing one. * <p>The default implementation throws an IllegalTransactionStateException, * assuming that participating in existing transactions is generally not * supported. Subclasses are of course encouraged to provide such support. * @param status the status representation of the transaction * @throws TransactionException in case of system errors */ protected void doSetRollbackOnly(DefaultTransactionStatus status) throws TransactionException { throw new IllegalTransactionStateException( "Participating in existing transactions is not supported - when 'isExistingTransaction' " + "returns true, appropriate 'doSetRollbackOnly' behavior must be provided"); } /** * Register the given list of transaction synchronizations with the existing transaction. * <p>Invoked when the control of the Spring transaction manager and thus all Spring * transaction synchronizations end, without the transaction being completed yet. This * is for example the case when participating in an existing JTA or EJB CMT transaction. * <p>The default implementation simply invokes the <code>afterCompletion</code> methods * immediately, passing in "STATUS_UNKNOWN". This is the best we can do if there's no * chance to determine the actual outcome of the outer transaction. * @param transaction transaction object returned by <code>doGetTransaction</code> * @param synchronizations List of TransactionSynchronization objects * @throws TransactionException in case of system errors * @see #invokeAfterCompletion(java.util.List, int) * @see TransactionSynchronization#afterCompletion(int) * @see TransactionSynchronization#STATUS_UNKNOWN */ protected void registerAfterCompletionWithExistingTransaction(Object transaction, List synchronizations) throws TransactionException { logger.debug("Cannot register Spring after-completion synchronization with existing transaction - " + "processing Spring after-completion callbacks immediately, with outcome status 'unknown'"); invokeAfterCompletion(synchronizations, TransactionSynchronization.STATUS_UNKNOWN); } /** * Cleanup resources after transaction completion. * <p>Called after <code>doCommit</code> and <code>doRollback</code> execution, * on any outcome. The default implementation does nothing. * <p>Should not throw any exceptions but just issue warnings on errors. * @param transaction transaction object returned by <code>doGetTransaction</code> */ protected void doCleanupAfterCompletion(Object transaction) { } //--------------------------------------------------------------------- // Serialization support //--------------------------------------------------------------------- private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { // Rely on default serialization; just initialize state after deserialization. ois.defaultReadObject(); // Initialize transient fields. this.logger = LogFactory.getLog(getClass()); } /** * Holder for suspended resources. * Used internally by <code>suspend</code> and <code>resume</code>. */ protected static class SuspendedResourcesHolder { private final Object suspendedResources; private List suspendedSynchronizations; private String name; private boolean readOnly; private Integer isolationLevel; private boolean wasActive; private SuspendedResourcesHolder(Object suspendedResources) { this.suspendedResources = suspendedResources; } private SuspendedResourcesHolder( Object suspendedResources, List suspendedSynchronizations, String name, boolean readOnly, Integer isolationLevel, boolean wasActive) { this.suspendedResources = suspendedResources; this.suspendedSynchronizations = suspendedSynchronizations; this.name = name; this.readOnly = readOnly; this.isolationLevel = isolationLevel; this.wasActive = wasActive; } } }