Under the hood @Transactional is pure Spring AOP: at startup Spring wraps each annotated bean in a proxy, and that proxy — not your class — is what gets injected everywhere. When a call comes in through the proxy, a TransactionInterceptor runs around your method and drives the transaction. No bytecode of your method is changed.
The pieces
@EnableTransactionManagement(auto-applied by Boot) registers anInfrastructureAdvisorAutoProxyCreator, aBeanPostProcessorthat, during context startup, detects beans with@Transactionaland replaces them with proxies.- The advisor's advice is
TransactionInterceptor, which on each invocation consults aPlatformTransactionManagerand the method's transaction attributes (propagation, isolation, rollback rules). - The interceptor's logic, in essence:
status = transactionManager.getTransaction(attrs) // begin or join, per propagation
try {
result = method.invoke() // run your code
} catch (ex) {
if (attrs.rollbackOn(ex)) transactionManager.rollback(status);
else transactionManager.commit(status);
throw ex;
}
transactionManager.commit(status); // normal return
return result;
JDK vs CGLIB proxies
Spring picks the proxy style automatically:
- JDK dynamic proxy if the bean implements an interface — the proxy implements the same interface.
- CGLIB subclass proxy if there's no interface (or
proxyTargetClass = true) — Spring generates a runtime subclass that overrides your methods. Boot defaults to CGLIB.
Either way the proxy intercepts only external calls that go through it. This is exactly why two limitations exist: this.method() self-calls bypass the proxy (the object calls its own real method, not the proxy), and private/final methods can't be advised (a CGLIB subclass can't override them, and there's no interface entry for a JDK proxy).