Proxies & AOP
Mid / SeniorJDK dynamic proxies vs CGLIB, the self-invocation trap, and the AOP vocabulary behind @Transactional, @Async and @Cacheable.
Created 5 October 2026 · Updated 23 October 2026
7 concepts · detail hidden
01Why most Spring "magic" is a proxy
#c-why-most-spring-magic-is-a-proxy@Transactional,@Async,@Cacheable,@Retryable,@PreAuthorize,@Validatedand@Repositoryexception translation are all implemented the same way: during bean creation (postProcessAfterInitialization) aBeanPostProcessorsees the annotation and replaces your bean with a proxy that wraps it. Every other bean gets the proxy injected. A call goes caller → proxy → interceptor chain (open transaction / check cache / submit to executor) → your real method → back through the interceptors.OrderController ──► [Proxy: TransactionInterceptor] ──► OrderService (real object) begin tx ▲ │ commit/rollback └───────┘Knowing this one fact explains nearly every “my annotation doesn’t work” bug.
02JDK dynamic proxies vs CGLIB proxies
#c-jdk-dynamic-proxies-vs-cglib-proxies- JDK dynamic proxy (
java.lang.reflect.Proxy) — implements the bean’s interfaces. Only interface methods are proxied, and the proxy is not an instance of your class, so injecting by the concrete class type fails. - CGLIB proxy — generates a subclass of your class at runtime and overrides its
methods. Works without interfaces. Cannot proxy
finalclasses orfinalmethods (can’t override them), andprivatemethods are never intercepted.
Plain Spring uses JDK proxies when the bean implements an interface; Spring Boot defaults to CGLIB for everything (
spring.aop.proxy-target-class=true) to avoid injection-by-class surprises. Kotlin classes arefinalby default, which is why thekotlin-spring(all-open) compiler plugin exists.- JDK dynamic proxy (
03The self-invocation problem
#c-the-self-invocation-problemA call from one method of a bean to another method of the same bean goes through
this, not through the proxy — so the annotation on the called method is silently ignored.@Service public class ReportService { public void generateAll() { for (var id : ids()) { generate(id); // this.generate(...) — bypasses the proxy } } @Transactional(propagation = Propagation.REQUIRES_NEW) public void generate(long id) { ... } // NOT in a new transaction when called above }Same story for
@Async(runs synchronously),@Cacheable(no caching) and@Retryable. Fixes, best first:- Move the method to another bean and inject it — the clean design fix.
- Inject yourself lazily (
@Lazy @Autowired ReportService self;) and callself.generate(). - Use programmatic control (
TransactionTemplate) instead of the annotation. - Switch to AspectJ compile-/load-time weaving (
mode = AdviceMode.ASPECTJ), which modifies the bytecode itself so eventhiscalls are intercepted — rarely worth it.
04Which methods a proxy can intercept
#c-which-methods-a-proxy-can-intercept- Public methods — always (for both proxy types).
- Protected / package-private — with CGLIB proxies, Spring 6+ honours
@Transactionalon them too; older versions and JDK proxies only handle public (interface) methods. - Private methods — never. A JDK proxy only implements interface methods and a CGLIB proxy
is a subclass, and neither can override a private method — so the call never passes through
the interceptor, and
@Transactional(or@Async,@Cacheable) on it is silently ignored. finalmethods — never with CGLIB; the call runs on the proxy instance itself, whose fields are uninitialised, so it may even NPE.- Static methods — never.
- Calls via
this— never (self-invocation).
05AOP vocabulary — aspect, advice, pointcut, join point, weaving
#c-aop-vocabulary-aspect-advice-pointcut-join-point-weaving- Join point — a point in execution where behaviour can be inserted. In Spring AOP this is always a method execution on a Spring bean.
- Pointcut — an expression selecting join points:
execution(* com.acme.service..*(..)),@annotation(com.acme.Audited),within(@org.springframework.stereotype.Service *). - Advice — the code that runs at selected join points, plus when it runs.
- Aspect — a class bundling pointcuts and advice (
@Aspect @Component). - Weaving — linking aspects into the target. Spring AOP weaves at runtime via proxies; full AspectJ weaves at compile- or load-time and can also intercept field access, constructors, and self-calls.
06Advice types
#c-advice-types@Aspect @Component public class TimingAspect { @Around("@annotation(com.acme.Timed)") public Object time(ProceedingJoinPoint pjp) throws Throwable { long t0 = System.nanoTime(); try { return pjp.proceed(); // call the real method } finally { log.info("{} took {} µs", pjp.getSignature(), (System.nanoTime() - t0) / 1000); } } }@Before— before the method; can block it only by throwing.@AfterReturning— after a normal return; can read the return value.@AfterThrowing— after an exception; can read it.@After— after either outcome (likefinally).@Around— wraps the call; decides whether/how to callproceed(), can change arguments and return values. The most powerful, and whatTransactionInterceptoris.
07Ordering of multiple aspects on one method
#c-ordering-of-multiple-aspects-on-one-methodWhen several proxies/advisors apply (say
@Transactionaland@Retryableon the same method), their nesting order matters. Order is set by@Order/Ordered; lower value = higher precedence = outermost. For retries you want the retry outside the transaction, so each attempt gets a fresh transaction — if the transaction is outermost, all retries reuse one transaction that has already been marked rollback-only. The@EnableTransactionManagement(order = …)attribute controls the transaction advisor’s position.