Why @Transactional May Not Work: Common Failure Scenarios and Source‑Code Analysis
This article explains three typical situations in which Spring's @Transactional annotation becomes ineffective—non‑public methods, internal self‑calls, and caught exceptions—illustrates each case with runnable code examples, and dives into the underlying AOP and transaction‑management source code to show why the proxy logic is bypassed.
@Transactional Annotation Failure Scenarios
Spring's @Transactional support is implemented through dynamic proxies; when certain conditions are not satisfied the proxy is not applied and the transaction does not start, causing database operations to be committed even when an exception occurs.
Scenario 1 – Non‑public method
If a method marked with @Transactional is declared with a visibility other than public, Spring ignores the annotation because the proxy only intercepts public methods.
/**
* @author zhoujy
*/
@Component
public class TestServiceImpl {
@Resource
TestMapper testMapper;
@Transactional
void insertTestWrongModifier() { // default (package‑private) visibility
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}
}Calling this method from another bean results in no transaction, so the insert before the exception is not rolled back.
Scenario 2 – Internal self‑call
When a @Transactional method is invoked from another method within the same class, the call bypasses the proxy (it uses this), so the transaction is never started.
/**
* @author zhoujy
*/
@Component
public class TestServiceImpl implements TestService {
@Resource
TestMapper testMapper;
@Transactional
public void insertTestInnerInvoke() {
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
}
public void testInnerInvoke() {
// internal call – no proxy involved
insertTestInnerInvoke();
}
}Running a test that calls testInnerInvoke() shows that the insert is not rolled back because the transaction never began.
Scenario 3 – Swallowing exceptions
If a @Transactional method catches the exception and does not re‑throw it, the transaction manager sees a successful execution and commits the changes.
/**
* @author zhoujy
*/
@Component
public class TestServiceImpl implements TestService {
@Resource
TestMapper testMapper;
@Transactional
public void insertTestCatchException() {
try {
int re = testMapper.insert(new Test(10,20,30));
if (re > 0) {
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
} catch (Exception e) {
System.out.println("i catch exception"); // exception swallowed
}
}
}The test confirms that even though an exception is thrown, the transaction is committed because the exception never propagates out of the method.
Underlying Spring Mechanism
Spring creates a BeanFactoryTransactionAttributeSourceAdvisor that scans bean methods for @Transactional. The
AbstractFallbackTransactionAttributeSource#computeTransactionAttributemethod returns null for non‑public methods, preventing proxy creation.
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
// Do not allow non‑public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// ... other logic omitted ...
}When the proxy intercepts a method, TransactionAspectSupport#invokeWithinTransaction obtains the transaction attribute, starts a transaction, invokes the target method, and either commits or rolls back based on whether an exception propagates.
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation) throws Throwable {
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
if (txAttr == null) {
return invocation.proceedWithInvocation(); // non‑transactional
}
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, methodIdentification(method, targetClass));
try {
Object retVal = invocation.proceedWithInvocation();
commitTransactionAfterReturning(txInfo);
return retVal;
} catch (Throwable ex) {
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
}
}Therefore, to make @Transactional work reliably you must ensure the method is public, invoke it through a Spring‑managed bean (or self‑inject the bean), and let exceptions propagate out of the transactional method.
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.
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.
