/*
* Copyright 2002-2010 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.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}.
*
* <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, JPA, 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
*/
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);
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.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 {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;
}
catch (RuntimeException ex) {
resume(null, suspendedResources);
throw ex;
}
catch (Error err) {
resume(null, suspendedResources);
throw err;
}
}
else {
// Create "empty" transaction: no actual transaction, but potentially synchronization.
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(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 prepareTransactionStatus(
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 {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;
}
catch (RuntimeException beginEx) {
resumeAfterBeginException(transaction, suspendedResources, beginEx);
throw beginEx;
}
catch (Error beginErr) {
resumeAfterBeginException(transaction, suspendedResources, beginErr);
throw beginErr;
}
}
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 =
prepareTransactionStatus(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.
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, null);
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;
}
}
// 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 != 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 prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null);
}
/**
* Create a new TransactionStatus for the given arguments,
* also initializing transaction synchronization as appropriate.
* @see #newTransactionStatus
* @see #prepareTransactionStatus
*/
protected final DefaultTransactionStatus prepareTransactionStatus(
TransactionDefinition definition, Object transaction, boolean newTransaction,
boolean newSynchronization, boolean debug, Object suspendedResources) {
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, newTransaction, newSynchronization, debug, suspendedResources);
prepareSynchronization(status, definition);
return status;
}
/**
* Create a rae TransactionStatus instance for the given arguments.
*/
protected DefaultTransactionStatus newTransactionStatus(
TransactionDefinition definition, Object transaction, boolean newTransaction,
boolean newSynchronization, boolean debug, Object suspendedResources) {
boolean actualNewSynchronization = newSynchronization &&
!TransactionSynchronizationManager.isSynchronizationActive();
return new DefaultTransactionStatus(
transaction, newTransaction, actualNewSynchronization,
definition.isReadOnly(), debug, suspendedResources);
}
/**
* Initialize transaction synchronization as appropriate.
*/
protected void prepareSynchronization(DefaultTransactionStatus status, TransactionDefinition definition) {
if (status.isNewSynchronization()) {
TransactionSynchronizationManager.setActualTransactionActive(status.hasTransaction());
TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(
(definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) ?
definition.getIsolationLevel() : null);
TransactionSynchronizationManager.setCurrentTransactionReadOnly(definition.isReadOnly());
TransactionSynchronizationManager.setCurrentTransactionName(definition.getName());
TransactionSynchronizationManager.initSynchronization();
}
}
/**
* 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<TransactionSynchronization> 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<TransactionSynchronization> 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<TransactionSynchronization> doSuspendSynchronization() {
List<TransactionSynchronization> suspendedSynchronizations =
TransactionSynchronizationManager.getSynchronizations();
for (TransactionSynchronization synchronization : suspendedSynchronizations) {
synchronization.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<TransactionSynchronization> suspendedSynchronizations) {
TransactionSynchronizationManager.initSynchronization();
for (TransactionSynchronization synchronization : suspendedSynchronizations) {
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<TransactionSynchronization> 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 if (!synchronizations.isEmpty()) {
// 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<TransactionSynchronization> 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 after completion of inner 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<TransactionSynchronization> 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<TransactionSynchronization> 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<TransactionSynchronization> 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;
}
}
}
|