Interview Prep Zoneby CuriouserLabs

Question 2 of 5 · staff level

JDK dynamic proxies vs CGLIB — differences, limitations, and the self-invocation problem.

🎤 Say this first

JDK proxies implement the target's interfaces via reflection (InvocationHandler) — proxy is-a interface, not is-a your class. CGLIB generates a subclass at runtime overriding public/protected methods — so final classes can't be proxied at all and final methods silently skip interception. Spring Boot defaults to CGLIB (proxyTargetClass=true) so class-typed injection works. Neither helps self-invocation: this.otherMethod() is a direct call on the raw instance — the proxy never sees it, so its annotations silently don't apply.

The full picture

JDK dynamic proxyCGLIB
Requires≥1 interfaceNon-final class, non-private methods
Proxy typeimplements interfaces onlysubclass of target
Inject by concrete class❌ ClassCastException risk
final methodsn/a (interface)silently NOT intercepted ⚠️
Constructionreflection-basedbytecode generation (objenesis avoids ctor side effects)
Self-invocation: the silent failure
@Service
public class ReportService {
    @Transactional
    public void generateAll() {
        for (var id : ids) generateOne(id);   // ❌ this.generateOne — direct call
    }
    @Transactional(propagation = REQUIRES_NEW)   // silently IGNORED above
    public void generateOne(long id) { … }
}

// Fixes, best first:
// 1. Move generateOne to a separate bean (design-honest: it IS a separate concern)
// 2. Inject self-proxy:  private final ReportService self;  (with @Lazy)
//    then: self.generateOne(id);
// 3. AspectJ weaving — heavy machinery, rarely worth it just for this

🔄 Likely follow-up questions

  • Why did Boot default to CGLIB proxies?
  • What does proxyTargetClass=true cost, if anything, on modern JVMs?
  • How does AspectJ compile-time weaving eliminate this class of bug?