Understanding Spring @Transactional: Common Transaction Pitfalls and Source‑Code Analysis
This article explains why Spring transactions may fail, demonstrates simple examples with and without the @Transactional annotation, details the annotation’s definition and attributes, and provides a step‑by‑step source‑code analysis of transaction creation, commit, rollback, and cleanup in Spring 4.3.12.
1. Small Transaction Examples
1.1 No @Transactional, Exception Not Rolled Back
Before executing the method, the database contains a record. The following code inserts a record and then throws a RuntimeException without any transaction annotation.
@Component
public class TransactionalTest {
@Resource
BasicPriceUploadRecordMapper basicPriceUploadRecordMapper;
public void onAddTransactionToException() {
BasicPriceUploadRecord base = new BasicPriceUploadRecord();
base.setErrorMsg("No transaction annotation, exception thrown");
base.setId(1824040965380245002L);
basicPriceUploadRecordMapper.updateByPrimaryKeySelective(base);
throw new RuntimeException();
}
}After execution the database still shows the inserted record, proving that the exception did not trigger a rollback.
1.2 Add @Transactional, Exception Rolled Back
Now the method is annotated with @Transactional, specifying the transaction manager and rollbackFor = Exception.
@Component
public class TransactionalTest {
@Resource
BasicPriceUploadRecordMapper basicPriceUploadRecordMapper;
/**
* Add declarative annotation, exception occurs
*/
@Transactional(transactionManager = "valuationTransactionManager", rollbackFor = Exception.class)
public void addTransactionToException() {
BasicPriceUploadRecord base = new BasicPriceUploadRecord();
base.setErrorMsg("Add @Transactional, exception thrown");
base.setId(1824040965380245002L);
basicPriceUploadRecordMapper.updateByPrimaryKeySelective(base);
throw new RuntimeException();
}
}After execution the database shows no change, confirming that the transaction was rolled back as expected.
2. @Transactional Annotation
2.1 Definition
@Transactional is a Spring annotation for declarative transaction management. It works through AOP and can be placed on interfaces, methods, classes, or public class methods.
2.2 Common Attributes
value|transactionManager: specify the transaction manager name
propagation: transaction propagation behavior
isolation: transaction isolation level
timeout: transaction timeout
readOnly: whether the transaction is read‑only
rollbackFor: exception classes that trigger rollback
noRollbackFor: exception classes that do NOT trigger rollback
Propagation behaviors:
REQUIRED – join existing transaction or create a new one
SUPPORTS – join if exists, otherwise execute non‑transactionally
MANDATORY – must join an existing transaction, else throw
REQUIRES_NEW – always start a new transaction, suspend the current one
NOT_SUPPORTED – execute non‑transactionally, suspend the current one
NEVER – must not run within a transaction, else throw
NESTED – execute within a nested transaction if a transaction exists
Isolation levels (default is DEFAULT):
DEFAULT – use the underlying DB default
READ_UNCOMMITTED – see uncommitted changes (dirty reads)
READ_COMMITTED – see only committed changes (prevents dirty reads)
REPEATABLE_READ – consistent reads within the transaction (prevents non‑repeatable reads)
SERIALIZABLE – highest isolation, prevents phantom reads3. Source‑Code Analysis (Spring 4.3.12)
3.1 Simple Transaction Flowchart
3.2 Proxy Class Generation
When a bean is instantiated and a @Transactional annotation is detected, Spring creates a transaction‑enhanced proxy. The creation chain is:
AbstractAutowireCapableBeanFactory.createBean => doCreateBean() => initializeBean() => applyBeanPostProcessorsAfterInitialization() => postProcessAfterInitialization() => AbstractAutoProxyCreator.postProcessAfterInitialization() => wrapIfNecessary() => createProxy() (proxyFactory.setProxyTargetClass(true))3.3 Method Entry in Proxy Class
The entry point is TransactionInterceptor.invoke(), which eventually calls TransactionAspectSupport.invokeWithinTransaction() to wrap the target method execution in a transaction.
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {
public Object invoke(final MethodInvocation invocation) throws Throwable {
Class<?> targetClass = invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null;
return this.invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
public Object proceedWithInvocation() throws Throwable {
return invocation.proceed();
}
});
}
}3.4 Core Transaction Logic
TransactionAspectSupport.invokeWithinTransaction()obtains the transaction attributes and manager, then distinguishes between declarative and programmatic transactions.
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final TransactionAspectSupport.InvocationCallback invocation) throws Throwable {
final TransactionAttribute txAttr = this.getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
final PlatformTransactionManager tm = this.determineTransactionManager(txAttr);
final String joinpointIdentification = this.methodIdentification(method, targetClass, txAttr);
if (txAttr != null && tm instanceof CallbackPreferringPlatformTransactionManager) {
// programmatic transaction handling (omitted)
} else {
TransactionAspectSupport.TransactionInfo txInfo = this.createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
this.completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
this.cleanupTransactionInfo(txInfo);
}
this.commitTransactionAfterReturning(txInfo);
return retVal;
}
}3.4.1 Starting a Transaction
TransactionAspectSupport.createTransactionIfNecessary()checks whether a transaction already exists and creates a new one if required.
protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) {
if (txAttr != null && ((TransactionAttribute)txAttr).getName() == null) {
txAttr = new DelegatingTransactionAttribute((TransactionAttribute)txAttr) {
public String getName() { return joinpointIdentification; }
};
}
TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
status = tm.getTransaction((TransactionDefinition)txAttr);
} else if (this.logger.isDebugEnabled()) {
this.logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + "] because no transaction manager has been configured");
}
}
return this.prepareTransactionInfo(tm, (TransactionAttribute)txAttr, joinpointIdentification, status);
}3.4.2 Rolling Back a Transaction
TransactionAspectSupport.completeTransactionAfterThrowing()decides whether to roll back based on the exception type and the rollback rules defined in the transaction attributes.
protected void completeTransactionAfterThrowing(TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.hasTransaction()) {
if (txInfo.transactionAttribute.rollbackOn(ex)) {
try { txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); }
catch (Throwable rollbackEx) { /* omitted */ }
} else {
try { txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); }
catch (Throwable commitEx) { /* omitted */ }
}
}
}3.4.3 Committing a Transaction
TransactionAspectSupport.commitTransactionAfterReturning()simply delegates to the transaction manager’s commit method when the transaction is still active.
protected void commitTransactionAfterReturning(TransactionInfo txInfo) {
if (txInfo != null && txInfo.hasTransaction()) {
txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
}
}3.4.4 Cleaning Up Transaction Information
After commit or rollback, AbstractPlatformTransactionManager.cleanupAfterCompletion() marks the transaction as completed, clears thread‑local synchronization, releases resources, and resumes any previously suspended transaction.
private void cleanupAfterCompletion(DefaultTransactionStatus status) {
status.setCompleted();
if (status.isNewSynchronization()) {
TransactionSynchronizationManager.clear();
}
if (status.isNewTransaction()) {
this.doCleanupAfterCompletion(status.getTransaction());
}
if (status.getSuspendedResources() != null) {
this.resume(status.getTransaction(), (SuspendedResourcesHolder)status.getSuspendedResources());
}
}Summary of Common Transaction Failures and Solutions
Self‑invocation within the same class: The call bypasses the proxy, so @Transactional is ignored. Solution: Move the logic to another bean or inject the bean into itself.
Checked exceptions not causing rollback: By default only RuntimeException triggers rollback. Solution: Specify rollbackFor = Exception.class or throw a RuntimeException.
Multi‑threaded operations: Each thread gets its own JDBC connection; the transaction context is thread‑local, so work in other threads is not part of the transaction. Solution: Use distributed transaction frameworks or manage rollback manually.
Incorrect propagation settings: Misusing REQUIRES_NEW, NOT_SUPPORTED, etc., can suspend or start unintended transactions. Solution: Choose the appropriate propagation behavior for each method.
When a method finishes normally, Spring calls commitTransactionAfterReturning(); when an exception occurs, it calls completeTransactionAfterThrowing(). Both methods first verify the existence of a transaction and then decide whether to commit or roll back based on the defined rules.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Zhuanzhuan Tech
A platform for Zhuanzhuan R&D and industry peers to learn and exchange technology, regularly sharing frontline experience and cutting‑edge topics. We welcome practical discussions and sharing; contact waterystone with any questions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
