What are the practical constraints and costs of CGLIB-based @Configuration enhancement, and how do they influence design in Kotlin and native-image builds?
answer
- CGLIB subclass = no final class, no private/final @Bean
- startup cost + runtime bytecode
- native image / AOT hate runtime proxies → proxyBeanMethods=false
- Kotlin final-by-default → kotlin-spring all-open plugin
- rule: inject params, never self-call @Bean methods
basics
~20 sFull-mode @Configuration is CGLIB-subclassed, so the class can't be final and @Bean methods can't be private/final. It adds startup cost and needs runtime bytecode, which hurts GraalVM native images. Kotlin needs the all-open plugin; teams often prefer proxyBeanMethods=false with parameter injection.
solid answer
~50 sCGLIB enhancement subclasses the @Configuration class to intercept @Bean calls, which imposes structural constraints: the class must be subclassable (non-final, accessible constructor) and @Bean methods must be overridable (not private or final); violations fail at startup. It also costs startup time and memory to generate/load proxies, and — critically — relies on runtime bytecode generation, which GraalVM native images and Spring AOT can't do at build time. Practically: in Kotlin, classes and methods are final by default, so @Configuration requires the kotlin-spring (all-open) compiler plugin to open them, otherwise CGLIB fails. For native/AOT and large apps, the framework and teams favor proxyBeanMethods=false plus strict method-parameter injection (no inter-bean self-calls), eliminating the proxy entirely. So the design rule becomes: don't rely on inter-bean method calls; inject dependencies as parameters, reserving full mode for the rare cases that truly need call interception.
code
kotlin · 14 lines// Requires the kotlin-spring (all-open) plugin so the class/methods
// are opened for CGLIB in full mode. Or avoid the proxy entirely:
@Configuration(proxyBeanMethods = false)
class MetricsConfig {
@Bean
fun meterRegistry(): MeterRegistry = SimpleMeterRegistry()
// Portable style: inject the singleton as a parameter,
// never call meterRegistry() directly.
@Bean
fun timedAspect(registry: MeterRegistry): TimedAspect =
TimedAspect(registry)
}go deeper
Know full-mode config is proxied so the class/methods can't be final/private; usually not expected to go deeper.
Explain the startup cost and the constraints, and that Kotlin needs a plugin for @Configuration.
Connect CGLIB to native-image/AOT limits and the proxyBeanMethods=false + parameter-injection remedy; know the Kotlin all-open detail.
Set org-wide conventions (no self-calls, params only, proxyBeanMethods=false for native), reason about migration risks, record/final edge cases, and proxy identity implications.
## Why CGLIB is used at all Full-mode `@Configuration` needs to intercept calls between `@Bean` methods so they return container singletons. Spring implements this by generating, at context startup, a **CGLIB subclass** of the configuration class that **overrides** each `@Bean` method. CGLIB is a bytecode library that creates a subclass at runtime. ## Structural constraints (hard failures) Because interception is via subclassing/overriding: - The `@Configuration` class **must not be `final`** (can't subclass a final class). - `@Bean` methods **must not be `private` or `final`** (can't override them). - The class needs a constructor CGLIB can invoke; overly restrictive constructors can cause issues. - The instance Spring registers is a **proxy subclass**, not your raw class — usually invisible, but relevant if you do `getClass()` reflection or identity checks. Violations produce startup exceptions (e.g. "@Bean method ... must not be private/final"). ## Costs - **Startup time & memory:** each full-mode config class yields a generated subclass to build, verify, and load. Multiplied across many auto-config classes it's measurable. - **Class-loading pressure / metaspace:** more generated classes. ## Native image & AOT — the big driver GraalVM **native images** compile ahead-of-time with a **closed world**: no runtime bytecode generation/reflection beyond what's registered. CGLIB's runtime subclassing is fundamentally incompatible. Spring's **AOT processing** (Boot 3 / Spring 6) pre-computes bean definitions at build time and also wants to avoid runtime proxies. The remedy is `proxyBeanMethods = false`, which removes enhancement while keeping the class as configuration. This is why Spring Boot's auto-configuration classes almost universally set `proxyBeanMethods = false` and rely on parameter injection. ## Kotlin implications Kotlin makes classes and members **`final` by default**. A `@Configuration` class in Kotlin would therefore be un-enhanceable and CGLIB would fail. The **`kotlin-spring`** compiler plugin (a preset of **all-open**) automatically opens classes annotated with Spring stereotypes (including `@Configuration`) and their methods, so full mode works. If you forget the plugin, you get CGLIB errors — or you can sidestep it entirely with `proxyBeanMethods = false` (no opening needed for interception, since there's none). ## The resulting design principle Because full mode's only unique feature is inter-bean **method-call** interception, and that feature carries all the cost, the modern guidance is: - **Never depend on inter-bean method calls** for shared singletons. - **Always inject dependencies as `@Bean` method parameters** — portable across full mode, lite mode, and `proxyBeanMethods=false`. - Prefer `proxyBeanMethods = false` for library/auto-config code and any native/AOT target. - Keep default full mode only where a legacy config genuinely relies on self-calls and native isn't a concern. ## Edge cases & gotchas - Migrating a config to `proxyBeanMethods=false` (or native) can silently break self-calling `@Bean` methods (duplicate instances, no error). Audit/lint for direct `@Bean` method calls first. - CGLIB constraints also mean you can't make a `@Configuration` a Kotlin `object` (singleton) and rely on enhancement, and you can't seal it. - Proxy identity: injecting the config class itself yields the enhanced subclass; equals/hashCode based on class identity can surprise. - Lombok/records: a Java `record` is implicitly `final`, so a record can't be a full-mode `@Configuration` (use a normal class or `proxyBeanMethods=false`).
- Why does a Kotlin @Configuration class typically require the kotlin-spring compiler plugin?Kotlin classes and methods are final by default. Full-mode @Configuration is CGLIB-subclassed, which needs the class open and @Bean methods overridable. The kotlin-spring (all-open) plugin auto-opens Spring-stereotyped classes; without it CGLIB enhancement fails. proxyBeanMethods=false is an alternative that removes the need.
- Can a Java record be a full-mode @Configuration class?No. Records are implicitly final, so CGLIB can't subclass them; full-mode enhancement fails. Use a regular non-final class, or set proxyBeanMethods=false to avoid enhancement.
- How would you make a large config codebase native-image ready with respect to @Bean methods?Set proxyBeanMethods=false everywhere, replace all inter-bean method calls with parameter injection, and lint/audit for remaining self-calls that would otherwise silently create duplicate instances. Rely on Spring AOT to pre-compute bean definitions.
saying these in an interview costs you the question
- Saying CGLIB proxies work fine in GraalVM native images — runtime bytecode generation is exactly what native can't do.
- Thinking Kotlin @Configuration 'just works' without knowing about the all-open/kotlin-spring plugin.
- Claiming full mode has no runtime/startup cost.
- Believing a final class or record can be full-mode enhanced.