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?
answer
- interfaces for all advised beans → JDK proxy
- proxyBeanMethods=false + param injection
- processAot + tracing agent + native CI build
- RuntimeHintsRegistrar for the gaps
- ban proxyTargetClass=true / final advised beans / TARGET_CLASS scope proxy
basics
~10 sDesign 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 sI'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// 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
Know the headline: design to interfaces and avoid CGLIB for native.
Add proxyBeanMethods=false, AOT hints, and manual RuntimeHintsRegistrar for gaps.
Sequence a migration: interface redesign, AOT verification, tracing agent, native CI.
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