skip to content

You lead a service being migrated to GraalVM native image and hitting proxy-related failures. What design and configuration strategy do you adopt to keep proxying reliable?

level: principalimportance: nice to knowfreq 28%

answer

  1. interfaces for all advised beans → JDK proxy
  2. proxyBeanMethods=false + param injection
  3. processAot + tracing agent + native CI build
  4. RuntimeHintsRegistrar for the gaps
  5. ban proxyTargetClass=true / final advised beans / TARGET_CLASS scope proxy

basics

~10 s

Design AOP/transaction targets behind interfaces so JDK proxies apply, set proxyBeanMethods = false, let Spring AOT emit proxy hints, and add RuntimeHintsRegistrar for the few proxies AOT can't infer. Avoid CGLIB/class proxies.

solid answer

~40 s

I'd shift the codebase toward proxyless/interface-based design. Concretely: (1) put every AOP-advised concern (`@Transactional`, `@Cacheable`, `@Async`, security) behind a business interface so Spring uses JDK dynamic proxies, whose interface sets AOT can pre-declare; (2) mark configuration `proxyBeanMethods = false` and inject collaborators as `@Bean` parameters; (3) let `processAot` in the Boot build plugin generate proxy + reflection hints, and validate with the GraalVM reachability agent plus a native test build in CI; (4) for proxies AOT can't discover (dynamic `ProxyFactory`, third-party clients) add `RuntimeHintsRegistrar`s wired via `@ImportRuntimeHints`; (5) forbid `proxyTargetClass = true` and final classes on advised beans in review. The governing principle: don't rely on runtime bytecode generation — make every needed proxy statically knowable at build time.

code

java · 23 lines
java
// Guardrail example: interface-based advised bean + explicit hints for a hand-built proxy
public interface InvoiceApi { Invoice issue(long id); }

@Service
class DefaultInvoiceService implements InvoiceApi {
    @Transactional  // JDK proxy over InvoiceApi; AOT pre-declares the interface set
    public Invoice issue(long id) { /* ... */ return new Invoice(id); }
}

// For a proxy AOT can't infer (e.g. built via ProxyFactory at runtime):
class GapHints implements RuntimeHintsRegistrar {
    public void registerHints(RuntimeHints hints, ClassLoader cl) {
        hints.proxies().registerJdkProxy(
            InvoiceApi.class,
            org.springframework.aop.SpringProxy.class,
            org.springframework.aop.framework.Advised.class,
            org.springframework.core.DecoratingProxy.class);
    }
}

@Configuration(proxyBeanMethods = false)
@ImportRuntimeHints(GapHints.class)
class InvoiceConfig { }

go deeper

for a junior

Know the headline: design to interfaces and avoid CGLIB for native.

for a middle

Add proxyBeanMethods=false, AOT hints, and manual RuntimeHintsRegistrar for gaps.

for a senior

Sequence a migration: interface redesign, AOT verification, tracing agent, native CI.

for a principal

Own the org-wide policy and trade-offs: guardrails in review/CI, third-party risk, and deciding which components stay JVM-only.

**The problem framing.** GraalVM native image is closed-world: no runtime class generation, reflection/proxy/resource access only if declared at build time. CGLIB (class proxies) fundamentally can't work — it defines subclasses at runtime. JDK dynamic proxies work *only* if their exact interface sets are registered. So a native migration is largely about eliminating unknowable, runtime-generated proxies. **Strategy, layer by layer.** 1. **Design to interfaces for all advised beans.** Anything Spring proxies for cross-cutting behavior — `@Transactional`, `@Cacheable`/`@CacheEvict`, `@Async`, `@Retryable`, method security, `@Validated` — should be reached through a business interface. Then Spring picks JDK dynamic proxies, and AOT can enumerate the interface set. Concrete-class targets force CGLIB. 2. **Lite-mode configuration.** `@Configuration(proxyBeanMethods = false)` removes the config-class CGLIB proxy. Pass shared beans as method parameters instead of calling sibling `@Bean` methods (which in lite mode would duplicate instances). Boot's own auto-config already does this. 3. **Lean on AOT, then verify.** The Boot build plugin's `processAot` refreshes the context in AOT mode and emits `RuntimeHints` (proxies, reflection, resources, serialization). Add the **GraalVM tracing agent** (`-agentlib:native-image-agent`) during representative test runs to capture dynamic behavior, and run an actual `nativeCompile`/`nativeTest` in CI so proxy gaps fail the build, not production. 4. **Fill gaps explicitly.** For proxies created outside the declarative model — hand-rolled `ProxyFactory`, MapStruct/OpenFeign-style clients, some library beans — implement `RuntimeHintsRegistrar` with `hints.proxies().registerJdkProxy(Api.class, SpringProxy.class, Advised.class, DecoratingProxy.class)` and register via `@ImportRuntimeHints` or `aot.factories`. 5. **Guardrails in review/CI.** Ban `spring.aop.proxy-target-class=true` and `@EnableTransactionManagement(proxyTargetClass = true)` where not required; flag `final` classes/methods on advised beans (they can't be class-proxied and are a Kotlin default — use `kotlin-spring` all-open); prefer constructor injection to keep beans interface-clean. **Edge cases.** - **Scoped proxies** (`@Scope(proxyMode = TARGET_CLASS)`) use CGLIB — switch to `INTERFACES` where feasible. - **Self-invocation** still bypasses proxies in native exactly as on the JVM — no change, but worth noting when refactoring. - **`spring-data` repositories** and **HTTP interface clients** are interface-proxied and handled by Boot AOT; custom infrastructure isn't. - **Third-party libraries** not AOT-aware are the biggest risk — budget for hints or replacement. **Trade-offs.** Interface-everywhere adds indirection and boilerplate; the payoff is deterministic native builds, faster startup, lower memory. If a component genuinely needs class proxying and can't be redesigned, it may have to stay JVM-only — a deployment-topology decision, not just a code one. **Governing principle.** Make every proxy statically discoverable at build time; never depend on runtime bytecode generation.

  • How would you catch missing proxy hints before production?
    Run an actual `nativeCompile`/`nativeTest` in CI so proxy failures break the build, and use the GraalVM tracing agent (`-agentlib:native-image-agent`) over representative integration tests to capture dynamically created proxies, feeding the metadata back into the image.
  • A legacy bean must be proxied as a concrete class and can't be redesigned to an interface. What are your options?
    Options: extract an interface if at all possible; if not, accept it may not be native-compatible and keep that service on the JVM; or, for scoped-proxy cases, switch `proxyMode` to `INTERFACES`. Class proxies can't be salvaged with hints because hints register interface sets, not generated subclasses.
  • Why is `@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)` a native concern?
    TARGET_CLASS uses a CGLIB subclass proxy, which needs runtime class generation and is unsupported in native image. Prefer `INTERFACES` mode where the bean has a suitable interface.

saying these in an interview costs you the question

  • Proposing to 'just add reflection hints' to make CGLIB work (impossible — needs runtime class definition)
  • Skipping a real native build in CI and trusting JVM tests to catch proxy gaps
  • Ignoring third-party/library proxies as the dominant migration risk
  • Leaving advised beans final (Kotlin) and expecting proxying to work

context