How do AOT-generated bean definitions change the handling of `@Configuration` classes and `@Bean` methods compared to normal CGLIB-based proxying?
answer
- CGLIB proxy = runtime bytecode → forbidden in native
- AOT: no config subclass, @Bean via forFactoryMethod
- inter-bean call becomes container-resolved argument
- singleton preserved without proxy
- prefer proxyBeanMethods=false
basics
~20 sNormally full @Configuration classes are CGLIB-enhanced so inter-@Bean calls return the shared singleton. In AOT mode there's no CGLIB subclass; the generated code calls @Bean methods directly and wires bean references through the container, preserving singleton semantics without a proxy.
solid answer
~40 sA `@Configuration(proxyBeanMethods=true)` class is normally subclassed by **CGLIB** at runtime so that a `@Bean` method calling another `@Bean` method returns the container-managed singleton rather than a fresh object. CGLIB subclassing requires runtime bytecode generation, which the native closed world forbids. AOT sidesteps this: the generated `*__BeanDefinitions` register each `@Bean` as its own definition with an instance supplier that uses `forFactoryMethod` and obtains dependencies from the `BeanFactory`. So an inter-bean dependency becomes a real container lookup/injected argument, not an intercepted self-call — singleton uniqueness is preserved structurally. Because there's no proxy, you should prefer `proxyBeanMethods=false` where possible; but AOT works either way. This also means the config class itself is registered as a plain (non-enhanced) bean.
code
java · 19 lines@Configuration // proxyBeanMethods=true by default
class AppConfig {
@Bean MyRepository repo() { return new MyRepository(); }
@Bean MyService service() { return new MyService(repo()); }
}
// Runtime (JVM, no AOT): CGLIB subclass of AppConfig intercepts the
// internal repo() call and returns the shared singleton.
// AOT-generated (native / spring.aot.enabled): no proxy — the container
// injects the singleton repo as an argument to the factory method:
public static BeanInstanceSupplier<MyService> serviceSupplier() {
return BeanInstanceSupplier
.<MyService>forFactoryMethod(AppConfig.class, "service", MyRepository.class)
.withGenerator((registeredBean, args) ->
registeredBean.getBeanFactory()
.getBean(AppConfig.class) // plain, NOT CGLIB-enhanced
.service(args.get(0))); // args.get(0) = container's singleton repo
}go deeper
Know that CGLIB proxying of @Configuration doesn't happen in native and AOT handles it differently.
Explain that AOT registers each @Bean via a factory-method supplier and injects singletons as arguments instead of proxying.
Contrast proxyBeanMethods true/false, explain how singleton semantics are preserved without a proxy, and build-time condition evaluation.
Discuss side-effect/idempotency implications of removing interception and guidance for library authors on lite vs full configuration under AOT.
## Why `@Configuration` normally needs CGLIB Consider: ```java @Configuration class AppConfig { @Bean MyRepository repo() { return new MyRepository(); } @Bean MyService service() { return new MyService(repo()); } // calls repo() directly } ``` With `proxyBeanMethods=true` (the default for `@Configuration`), Spring creates a **CGLIB subclass** of `AppConfig` at runtime. That subclass intercepts the `repo()` call inside `service()` and routes it to the container, returning the **same singleton** `MyRepository` instead of a new one each time. This 'full' configuration mode relies on **runtime bytecode enhancement**. ## The native/closed-world problem GraalVM native image cannot generate the CGLIB subclass at runtime — no dynamic class loading/definition. So the interception mechanism is unavailable. ## What AOT generates instead AOT registers each `@Bean` method result as its own `BeanDefinition` whose instance supplier uses `BeanInstanceSupplier.forFactoryMethod(AppConfig.class, "service", MyRepository.class)`. The generator resolves the method's arguments **from the container** and calls the method on the (plain, non-enhanced) config bean: ```java registeredBean.getBeanFactory().getBean(AppConfig.class).service(args.get(0)) ``` Here `args.get(0)` is the **container's** `MyRepository` singleton, injected as a parameter — not the result of an intercepted internal `repo()` call. So the inter-bean reference is satisfied by dependency resolution, and singleton semantics are preserved **without any proxy**. The config class is registered as an ordinary bean; it is **not** CGLIB-enhanced in AOT mode. ## Implication: `proxyBeanMethods` - With `proxyBeanMethods=false` ('lite' mode), the class is never proxied even at runtime; each `@Bean` method is a plain factory method and internal method calls are NOT intercepted. Best practice is to set `false` whenever a `@Bean` method doesn't call sibling `@Bean` methods — it's cheaper and AOT-friendly. - With `proxyBeanMethods=true`, on the JVM you still get CGLIB; but under AOT the generated code achieves the same singleton guarantee via container-resolved arguments, so the semantics match. ## Gotchas - If your `@Bean` method **directly calls** another `@Bean` method (`service() { return new MyService(repo()); }`) and you rely on `proxyBeanMethods=true` for singleton sharing, that logic still works under AOT because AOT rewires it through the container — but you should understand it's no longer literal method interception. - Side effects placed in a `@Bean` method that assumed single invocation via the proxy could behave differently if you switch to `proxyBeanMethods=false` on the JVM (each call runs the method). AOT itself keeps singleton behavior. - Component/`@Configuration` scanning is replaced by explicit registration, so conditions (`@ConditionalOn...`, profiles) are evaluated at **build time** during AOT processing — a config bean excluded at build time won't exist at runtime. - The removal of CGLIB is one reason AOT/native startups are faster and need fewer reflection/proxy hints.
- Under AOT, is the `@Configuration` class still CGLIB-enhanced?No. The config class is registered as a plain bean. Singleton sharing for inter-`@Bean` references is achieved by resolving those references from the container as method arguments, not by proxy interception.
- Given AOT rewires inter-bean calls, why still prefer `proxyBeanMethods=false`?On the JVM (non-AOT) it avoids CGLIB proxy creation and its cost, and it makes intent explicit. It's the recommended default when a `@Bean` method doesn't call sibling `@Bean` methods; AOT works with either setting.
saying these in an interview costs you the question
- Saying native image still generates a CGLIB subclass at runtime
- Claiming AOT breaks singleton semantics for inter-@Bean references
- Thinking proxyBeanMethods=true is required for @Bean methods to work under AOT
- Believing @Configuration conditions are evaluated at runtime in AOT mode (they're build-time)