Why Does @Transactional Fail? Common Pitfalls and Fixes
This article explains the main reasons why Spring's @Transactional annotation may become ineffective—including non‑public method modifiers, internal method calls, and swallowed exceptions—provides code examples, test cases, and a deep dive into the underlying proxy and AOP mechanisms.
Transactional Failure Scenarios
First case: When a method annotated with @Transactional has a non‑public modifier, the annotation will not take effect. Example code:
/**
* @author zhoujy
*/
@Component
public class TestServiceImpl {
@Resource
TestMapper testMapper;
@Transactional
void insertTestWrongModifier() {
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 in the same package:
@Component
public class InvokcationService {
@Resource
private TestServiceImpl testService;
public void invokeInsertTestWrongModifier() {
// call the non‑public @Transactional method
testService.insertTestWrongModifier();
}
}Test case:
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
@Resource
InvokcationService invokcationService;
@Test
public void testInvoke() {
invokcationService.invokeInsertTestWrongModifier();
}
}Because the method is not public, the transaction is not started, so the insert operation is not rolled back. Making the method public restores normal transaction behavior.
Second Scenario
Calling a @Transactional method from within the same class also prevents the transaction from starting. Example:
/**
* @author zhoujy
*/
@Component
public class TestServiceImpl implements TestService {
@Resource
TestMapper testMapper;
@Transactional
public void insertTestInnerInvoke() {
// normal public transactional method
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 to transactional method
insertTestInnerInvoke();
}
}Test case demonstrates that the external call works (transaction opens), while the internal call does not (transaction stays inactive).
Third Scenario
If a transactional method catches an exception without re‑throwing it, the transaction will not roll back:
/**
* @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 during execution
throw new NeedToInterceptException("need intercept");
}
testMapper.insert(new Test(210,20,30));
} catch (Exception e) {
System.out.println("i catch exception");
}
}
}Test case shows that although an exception is thrown, it is caught inside the method, so the insert of the second record is not rolled back.
Analysis of Why @Transactional Does Not Work
First Reason
The @Transactional annotation relies on dynamic proxies. If a method is not public, Spring’s transaction attribute source returns null, so no proxy is created for that method.
Spring checks each bean with BeanFactoryTransactionAttributeSourceAdvisor. It scans methods for @Transactional metadata; non‑public methods are ignored, preventing proxy creation.
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
if (!pc.getClassFilter().matches(targetClass)) {
return false;
}
MethodMatcher methodMatcher = pc.getMethodMatcher();
if (methodMatcher == MethodMatcher.TRUE) {
return true;
}
// iterate over class methods
Set<Class<?>> classes = new LinkedHashSet<>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
classes.add(targetClass);
for (Class<?> clazz : classes) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
for (Method method : methods) {
if (methodMatcher.matches(method, targetClass)) {
return true;
}
}
}
return false;
}If all methods are non‑public, Spring does not create a proxy at all.
When a class contains only non‑public methods, the resulting bean is not a proxy.
Proxy Not Invoked
If a class has both public and non‑public @Transactional methods, a proxy is created because of the public method, but the non‑public method still does not trigger transaction logic.
@Component
public class TestServiceImpl implements TestService {
@Resource
TestMapper testMapper;
@Transactional
public void insertTest() {
// transactional logic
}
@Transactional
void insertTestWrongModifier() {
// this will not start a transaction
}
}The proxy intercepts calls to the public method but not to the package‑private one.
Second Reason
Internal calls to @Transactional methods bypass the proxy because they use this instead of the proxy instance.
Since transaction management is based on dynamic proxies, an internal call does not go through the proxy, so the transaction logic is skipped.
A workaround is to inject the bean into itself and call the method via the injected proxy:
@Component
public class TestServiceImpl implements TestService {
@Resource
TestMapper testMapper;
@Resource
TestServiceImpl self;
@Transactional
public void insertTestInnerInvoke() {
// transactional work
}
public void testInnerInvoke() {
// internal call via proxy
self.insertTestInnerInvoke();
}
}Third Reason
If a transactional method catches its own exception and does not re‑throw it, Spring never sees the exception, so the transaction is not rolled back.
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();
}
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);
}
}Because the catch block in the method consumes the exception, the above logic never reaches the completeTransactionAfterThrowing step, and the transaction remains committed.
In summary, @Transactional may fail due to non‑public method modifiers, internal method calls that bypass the proxy, or exceptions that are caught and not re‑thrown.
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.
Java Interview Crash Guide
Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.
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.
