Skip to content

Proxies & AOP

Mid / Senior

JDK 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

  1. 01Why most Spring "magic" is a proxy

    @Transactional, @Async, @Cacheable, @Retryable, @PreAuthorize, @Validated and @Repository exception translation are all implemented the same way: during bean creation (postProcessAfterInitialization) a BeanPostProcessor sees 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.

    #c-why-most-spring-magic-is-a-proxy
  2. 02JDK 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 final classes or final methods (can’t override them), and private methods 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 are final by default, which is why the kotlin-spring (all-open) compiler plugin exists.

    #c-jdk-dynamic-proxies-vs-cglib-proxies
  3. 03The self-invocation problem

    A 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:

    1. Move the method to another bean and inject it — the clean design fix.
    2. Inject yourself lazily (@Lazy @Autowired ReportService self;) and call self.generate().
    3. Use programmatic control (TransactionTemplate) instead of the annotation.
    4. Switch to AspectJ compile-/load-time weaving (mode = AdviceMode.ASPECTJ), which modifies the bytecode itself so even this calls are intercepted — rarely worth it.
    #c-the-self-invocation-problem
  4. 04Which methods a proxy can intercept
    • Public methods — always (for both proxy types).
    • Protected / package-private — with CGLIB proxies, Spring 6+ honours @Transactional on 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.
    • final methods — 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).
    #c-which-methods-a-proxy-can-intercept
  5. 05AOP 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.
    #c-aop-vocabulary-aspect-advice-pointcut-join-point-weaving
  6. 06Advice 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 (like finally).
    • @Around — wraps the call; decides whether/how to call proceed(), can change arguments and return values. The most powerful, and what TransactionInterceptor is.
    #c-advice-types
  7. 07Ordering of multiple aspects on one method

    When several proxies/advisors apply (say @Transactional and @Retryable on 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.

    #c-ordering-of-multiple-aspects-on-one-method