skip to content

What are the practical constraints and costs of CGLIB-based @Configuration enhancement, and how do they influence design in Kotlin and native-image builds?

level: principalimportance: nice to knowfreq 26%

answer

  1. CGLIB subclass = no final class, no private/final @Bean
  2. startup cost + runtime bytecode
  3. native image / AOT hate runtime proxies → proxyBeanMethods=false
  4. Kotlin final-by-default → kotlin-spring all-open plugin
  5. rule: inject params, never self-call @Bean methods

basics

~20 s

Full-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 s

CGLIB 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
kotlin
// 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

for a junior

Know full-mode config is proxied so the class/methods can't be final/private; usually not expected to go deeper.

for a middle

Explain the startup cost and the constraints, and that Kotlin needs a plugin for @Configuration.

for a senior

Connect CGLIB to native-image/AOT limits and the proxyBeanMethods=false + parameter-injection remedy; know the Kotlin all-open detail.

for a principal

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.

context