In a Kotlin Spring app targeting native image, why can final classes break proxying, and how is it addressed?
answer
- Kotlin final by default → CGLIB can't subclass
- kotlin-spring = all-open plugin opens @Component/@Transactional/etc.
- data class can't be open
- native: interfaces + JDK proxy beats opening classes
- all-open is JVM ergonomics, not a native fix
basics
~10 sKotlin classes/methods are final by default. CGLIB can't subclass final classes, so class-based proxies fail. The kotlin-spring (all-open) compiler plugin makes Spring-annotated classes open. Better still, use interfaces so JDK proxies apply.
solid answer
~40 sKotlin makes every class and method `final` unless you write `open`. CGLIB proxies work by subclassing and overriding, so a final class or method can't be class-proxied at all — you'd get a startup failure or silently missing advice. Spring Boot's Kotlin support pulls in the `kotlin-spring` compiler plugin (an all-open configuration) that automatically opens classes annotated with `@Component`, `@Configuration`, `@Transactional`, etc., so CGLIB can subclass them on the JVM. For native image, though, CGLIB itself is unsupported, so the cleaner answer is to define advised beans behind interfaces and use JDK dynamic proxies, whose interface sets AOT pre-declares. So Kotlin's finality is one more reason Spring 6 steers toward interface/proxyless designs.
code
kotlin · 9 lines// JVM: kotlin-spring makes this open so CGLIB can proxy @Transactional on a class.
// Native-friendly: put the advised method behind an interface -> JDK proxy, no CGLIB.
interface ReportApi { fun generate(id: Long): Report }
@Service
class DefaultReportService : ReportApi {
@Transactional
override fun generate(id: Long): Report = Report(id)
}go deeper
Know Kotlin classes are final by default and CGLIB needs open classes; kotlin-spring opens them.
Explain all-open's annotation triggers and that native prefers interface/JDK proxies anyway.
Contrast JVM openness fix vs native CGLIB unsupported, and steer to interface design.
Set team conventions: interfaces for advised beans, avoid data-class proxy targets, treat all-open as JVM-only ergonomics.
**Kotlin's default finality.** Unlike Java, Kotlin declares classes and members `final` by default; you must explicitly mark them `open` to allow subclassing/overriding. This is a deliberate Kotlin design choice favoring safe inheritance. **Why it collides with proxying.** Spring's **CGLIB** proxies are *subclasses* that override the target's methods to weave in advice (`@Transactional`, `@Cacheable`, etc.). You cannot subclass a `final` class, nor override a `final` method. So a final Kotlin bean that Spring tries to CGLIB-proxy fails — historically an error like 'Cannot subclass final class', or advice that silently doesn't apply on final methods. **The JVM fix: `kotlin-spring` (all-open).** Spring Boot's Gradle/Maven Kotlin setup applies the `kotlin-spring` plugin, a preset of the Kotlin **all-open** compiler plugin. It automatically makes classes (and their members) `open` when annotated with Spring stereotypes: `@Component`, `@Async`, `@Transactional`, `@Cacheable`, `@Configuration`, `@SpringBootTest`, and meta-annotated variants (so `@Service`, `@RestController`, etc. are covered). That lets CGLIB subclass them without you sprinkling `open` everywhere. **Native-image twist.** All-open solves the *JVM* CGLIB case, but CGLIB is unsupported in GraalVM native image regardless of openness — the blocker there is runtime subclass *generation*, not finality. So for native, opening the class doesn't help a class proxy; you want **interface-based JDK proxies**, which sidestep both problems: interfaces don't require the impl to be open, and AOT can pre-declare the interface set. This reinforces Spring 6's proxyless/interface preference. **Gotchas.** - `data class`es can't be `open` — don't use them as CGLIB proxy targets; put behavior behind an interface instead. - `@Configuration` in full mode needs the class open (all-open handles it), but prefer `proxyBeanMethods = false` to avoid the config proxy entirely. - Even with all-open, self-invocation bypasses proxies (JVM and native alike). - The all-open plugin only opens *annotated* classes; a plain final helper you try to proxy manually stays final. **When to use.** Keep `kotlin-spring` for JVM ergonomics, but for native builds design advised beans as interfaces. Treat all-open as a convenience for the JVM path, not a native-image solution.
- What does the kotlin-spring compiler plugin actually do?It's an all-open preset that automatically marks classes (and members) `open` when they carry Spring annotations like `@Component`, `@Configuration`, `@Transactional`, `@Async`, `@Cacheable` (and meta-annotated stereotypes), so CGLIB can subclass them on the JVM without manual `open` keywords.
- Does making a Kotlin class open make it work with CGLIB in native image?No. Openness fixes the JVM finality problem, but CGLIB is unsupported in native image because it generates subclasses at runtime. For native you need interface-based JDK proxies instead.
saying these in an interview costs you the question
- Thinking all-open makes CGLIB work in native image (it only fixes JVM finality)
- Trying to CGLIB-proxy a Kotlin data class (can't be open)
- Believing you must manually add `open` everywhere despite kotlin-spring