What are the practical limitations of CGLIB proxying, and how would you decide between JDK and CGLIB proxies across a codebase?
answer
- CGLIB can't do final/private/static
- Objenesis skips the constructor
- JDK needs interface, advises interface methods only
- neither fixes self-invocation -> AspectJ weaving
- standardize one strategy, document it
basics
~20 sCGLIB subclasses the target, so it can't proxy final classes or override final/private/static methods, and it instantiates without your constructor (via Objenesis). JDK proxies need interfaces and only advise interface methods. Choose CGLIB for flexibility (Boot's default) or JDK to enforce interface boundaries.
solid answer
~40 sCGLIB works by generating a runtime subclass, which imposes limits: it cannot subclass final classes, cannot override final, private, or static methods (advice silently skips them), and it instantiates the proxy via Objenesis without running your constructor normally — so constructor side effects and final-field initialization can behave unexpectedly. It also adds small bytecode-generation and memory overhead. JDK proxies avoid subclassing but require interfaces and only intercept interface-declared methods. For decision-making: Spring Boot defaults to CGLIB (spring.aop.proxy-target-class=true) for robustness and inject-by-class convenience, and that's a fine default for most apps. Prefer JDK proxies when you want to enforce program-to-interface discipline, keep beans trivially mockable, or must proxy classes you can't make non-final. Neither choice fixes self-invocation; that needs AspectJ weaving or refactoring.
code
java · 13 lines// CGLIB pitfall: final method is silently NOT advised
@Service
class ReportService {
@Transactional
public final void generate() { // 'final' => CGLIB can't override => no transaction!
// ...
}
}
// For advice on internal/final/private targets, use AspectJ weaving instead of proxies:
@Configuration
@EnableLoadTimeWeaving // requires an AspectJ weaving agent
class WeavingConfig { }go deeper
Know CGLIB can't handle final classes/methods.
List CGLIB limits (final/private/static) and JDK's interface requirement.
Explain Objenesis constructor behavior and when silent missing-advice happens.
Set and enforce a codebase-wide proxy strategy, and reach for AspectJ weaving when proxies fundamentally can't cover the case.
## How CGLIB works and what it forbids CGLIB (repackaged as `org.springframework.cglib`) creates a **dynamic subclass** of the target and overrides methods to weave advice. Because it relies on subclassing/overriding, it **cannot**: - Proxy a **`final` class** (can't subclass it) — Spring fails or the class can't be advised. - Override **`final` methods** — they run un-advised. - Advise **`private` methods** — not overridable, so no interception. - Advise **`static` methods** — not part of instance dispatch. - Reliably advise **package-private** methods across packages. When advice silently doesn't apply, `@Transactional`/`@Cacheable` appear to 'not work' with no error — a classic debugging trap. ## Instantiation via Objenesis Modern Spring uses **Objenesis** to create the CGLIB subclass **without invoking a declared constructor**. Historically CGLIB required a **no-arg constructor**; Objenesis removed that requirement. But the practical consequence is that **your constructor logic and field initializers may not run** on the proxy instance the way you expect. Beans that do meaningful work in constructors, or rely on `final` field initialization at construction, can behave subtly wrong. Prefer keeping construction side-effect-free. ## JDK proxy limits (the other side) - Requires the target to **implement at least one interface**. - Only **interface-declared methods** are advised; public methods absent from the interface are invisible to the proxy. - The proxy is **not** assignable to the concrete class, breaking inject-by-impl and `getBean(Impl.class)`. ## Costs - CGLIB: bytecode generation at startup, extra classes/metaspace, and the constructor caveat above. - JDK: negligible generation cost but forces interface discipline and can't advise non-interface public API. ## Decision framework across a codebase 1. **Default to Boot's CGLIB** (`spring.aop.proxy-target-class=true`) for most services: fewer footguns, inject-by-class works, advises all public methods. Ensure classes/methods that need advice are **non-final**. 2. **Choose JDK proxies** (`spring.aop.proxy-target-class=false`) when you want to **enforce program-to-interface** architecture, keep dependencies mockable by interface, or must integrate with **`final`** classes you can't modify. Then mandate injection by interface. 3. **Neither solves self-invocation.** If aspects must apply to internal calls, or to `final`/`private`/`static` targets, use **AspectJ compile-time or load-time weaving** (`@EnableLoadTimeWeaving` / AspectJ weaver), which weaves bytecode directly and isn't proxy-based. 4. **Consistency over cleverness.** Pick one strategy per service and document it; mixing per-annotation `proxyTargetClass` overrides with the global property leads to confusing, hard-to-reason proxies. ## Governance angle At scale, standardize the property in a shared parent/starter, add an ArchUnit or lint rule that beans are injected by interface if you run JDK proxies, and code-review for `final` on classes that carry `@Transactional`/aspects when on CGLIB.
- A @Transactional method is final and nothing rolls back. Root cause under CGLIB?CGLIB proxies by subclassing and overriding; a final method can't be overridden, so the transactional advice is never woven and the method runs without a transaction. Remove final or use AspectJ weaving.
- How would you enforce interface-based injection if you switch a service to JDK proxies?Set spring.aop.proxy-target-class=false, require constructor injection by interface type, and add an ArchUnit/lint rule forbidding autowiring of *Impl types, plus code review.
saying these in an interview costs you the question
- Claiming CGLIB can advise private or static methods
- Saying CGLIB always requires a no-arg constructor (Objenesis removed that)
- Believing switching proxy type fixes self-invocation
- Thinking JDK proxies can advise methods not on an interface