skip to content

Why are CGLIB (class-based) proxies problematic in a GraalVM native image, and what does Spring prefer instead?

level: juniorimportance: must knowfreq 62%

answer

  1. CGLIB = runtime subclass generation → closed-world forbids it
  2. JDK proxy = interfaces pre-declared in reachability metadata → OK
  3. proxyBeanMethods=false avoids config CGLIB
  4. program to interfaces for native AOP
  5. final class/method → no CGLIB ever

basics

~10 s

CGLIB creates proxy classes at runtime by generating new subclasses. A native image is closed at build time and cannot generate classes at runtime, so CGLIB proxies fail. Spring prefers JDK/interface-based proxies instead.

solid answer

~40 s

CGLIB works by generating a new subclass of your bean's class at runtime using bytecode generation. GraalVM native image performs closed-world analysis at build time and forbids runtime class definition, so CGLIB's dynamic subclassing is unsupported. JDK dynamic proxies are also runtime-generated but Spring's AOT engine can pre-declare the exact interface sets they need in reachability metadata, so they work. That asymmetry is why Spring 6 favors interface-based (proxyless) approaches: use `proxyBeanMethods = false` on `@Configuration`, program to interfaces for AOP targets, and rely on the AOT-generated proxy hints. When you must proxy a concrete class, the class must be reachable and non-final, but it's a fragile path — designing to interfaces avoids the problem entirely.

code

java · 17 lines
java
// Native-friendly: lite-mode config avoids CGLIB proxying the @Configuration class
@Configuration(proxyBeanMethods = false)
class AppConfig {
    @Bean
    PaymentService paymentService() { return new DefaultPaymentService(); }
}

// AOP target designed as an interface -> JDK dynamic proxy, AOT can pre-declare it
public interface PaymentService {
    void charge(long cents);
}

@Service
class DefaultPaymentService implements PaymentService {
    @Transactional            // proxied via the PaymentService interface
    public void charge(long cents) { /* ... */ }
}

go deeper

for a junior

Know CGLIB generates subclasses at runtime and native image can't do that; JDK/interface proxies are preferred.

for a middle

Explain closed-world assumption, and that JDK proxies work because interface sets are pre-declared as hints.

for a senior

Tie to proxyBeanMethods=false, interface-based AOP, and how Spring AOT emits proxy hints via RuntimeHints.

for a principal

Discuss architectural design-to-interfaces policy, RuntimeHintsRegistrar for edge cases, and trade-offs of concrete-class proxying in native builds.

**Proxies in Spring.** A proxy is a stand-in object that wraps a real bean to add behavior (transactions via `@Transactional`, security via `@PreAuthorize`, caching via `@Cacheable`, scoped beans, `@Configuration` lite-vs-full mode). Spring has two proxy technologies: - **JDK dynamic proxies** — built into the JDK (`java.lang.reflect.Proxy`). They implement one or more *interfaces*; the proxy IS-A of every interface but is NOT a subclass of your concrete class. Requires the target to expose an interface. - **CGLIB proxies** — a bytecode library (bundled inside `spring-core`). They generate a *subclass* of the concrete class at runtime and override its methods. Used when a bean has no interface, or when `proxyTargetClass = true`. **Why CGLIB breaks in native image.** GraalVM `native-image` compiles ahead-of-time under a **closed-world assumption**: every class, method, and reflective access must be known at build time. There is no JIT and no runtime class loading/definition. CGLIB *defines brand-new classes at runtime* (`ClassLoader.defineClass`), which is exactly what the closed world forbids. So CGLIB-based proxies cannot be materialized in a native image. **Why JDK proxies still work.** JDK dynamic proxies are also generated at runtime, but GraalVM has explicit support for them *provided the exact set of interfaces is declared at build time* (a `proxy-config.json` / reachability-metadata entry). Spring's **AOT processing** (`spring-context` AOT, run by the `org.springframework.boot` Gradle/Maven plugin's `processAot` task) walks the bean factory during the build and emits these proxy hints via `RuntimeHints.proxies()`, so each needed interface combination is pre-registered. **Spring 6 / Boot 3 consequences.** - `@Configuration(proxyBeanMethods = false)` ("lite mode") avoids CGLIB-proxying your config class. Spring Boot auto-configuration uses this. Full mode (`true`, the default) CGLIB-proxies the config class so intra-config `@Bean` method calls return the singleton — that CGLIB proxy is a native-image hazard, so lite mode is preferred. - For AOP, program to interfaces so JDK proxies apply. `spring.aop.proxy-target-class` defaults to `true` (CGLIB) on the JVM; for native you steer toward interface proxies where possible. - If you genuinely must proxy a concrete class in native, that class must be **reachable, non-final, with non-final methods**, and hints must exist — a brittle path. **Gotchas.** Final classes/methods can never be CGLIB-proxied (subclassing is impossible) — a Kotlin gotcha since Kotlin classes are final by default (hence the `kotlin-spring`/all-open plugin). Self-invocation still bypasses proxies regardless of technology. `RuntimeHintsRegistrar` lets you add proxy hints manually when the AOT engine can't infer them. **When to use what.** Default to interfaces + `proxyBeanMethods = false` for native-friendly designs; reserve class proxies for JVM-only deployments where AOP must target concrete classes.

  • What does `proxyBeanMethods = false` change, and why does it matter for native?
    It puts the `@Configuration` class in 'lite' mode so Spring does NOT CGLIB-proxy it. `@Bean` methods are then plain factory methods (inter-bean method calls no longer route through the singleton cache). It matters because the CGLIB proxy that full mode creates is unsupported in native image; lite mode removes that hazard.
  • Can a JDK dynamic proxy proxy a class that has no interface?
    No. JDK proxies only implement interfaces; they can't subclass a concrete class. Without an interface Spring must fall back to CGLIB, which is the native-unfriendly path.

saying these in an interview costs you the question

  • Claiming CGLIB works in native if you just add reflection hints (it needs runtime class definition, which is forbidden)
  • Thinking JDK proxies need no build-time metadata (they need the interface set pre-declared)
  • Believing 'AOT just makes it faster' with no behavioral/proxy constraints

context