Final-by-default breaks Spring proxies and Mockito by default. How does the Kotlin ecosystem reconcile final-by-default with frameworks that need to subclass your classes?
answer
- Final classes break CGLIB proxies / classic mocks
- all-open plugin opens annotated classes
- kotlin-spring = all-open preset for Spring stereotypes
- no-arg / kotlin-jpa is a separate constructor concern
- MockK / mockito-inline mock final without opening
basics
~10 sMany frameworks create runtime subclasses (proxies/mocks), which final classes block. Kotlin provides compiler plugins like all-open (and kotlin-spring) that automatically open annotated classes so those tools work.
solid answer
~40 sFrameworks like Spring (CGLIB proxies for `@Transactional`, `@Configuration`) and Mockito generate subclasses at runtime; Kotlin's final-by-default would make them fail because you can't subclass a final class. The fix is the **all-open** compiler plugin: it makes classes (and members) annotated with configured annotations implicitly `open` without you writing the keyword. The **kotlin-spring** plugin is a preset of all-open that opens classes carrying Spring stereotypes (`@Component`, `@Service`, `@Transactional`, `@Configuration`, etc.). For JPA there's the **no-arg / kotlin-jpa** plugin (separate concern: synthesizes a no-arg constructor). For testing, **mockito-inline** / **mockk** mock final classes via bytecode instrumentation, avoiding subclassing entirely. Principals weigh opening-for-frameworks against API stability — prefer interfaces/`open` only where a true extension point exists.
code
kotlin · 10 lines// build.gradle.kts
plugins {
kotlin("plugin.spring") version "2.0.0" // opens @Service/@Transactional/etc.
}
@Service
class BillingService { // implicitly open thanks to the plugin
@Transactional
fun charge() { /* proxied */ }
}go deeper
Knows final classes can't be subclassed; may not connect this to frameworks.
Knows the all-open/kotlin-spring plugin exists and why Spring needs it.
Distinguishes all-open vs no-arg, explains CGLIB proxying and final-method pitfalls, and modern final-class mocking.
Treats opening-for-frameworks as a trade-off, steering domain code toward interfaces and minimal genuine extension points.
## The conflict Many JVM frameworks work by **subclassing your class at runtime** to inject behavior: - **Spring AOP** creates **CGLIB** proxies for `@Transactional`, `@Cacheable`, `@Configuration` beans by subclassing and overriding methods. - **Mockito** (classic) builds mock subclasses. - **Hibernate/JPA** uses proxies for lazy loading. Kotlin classes/methods are final by default, so these subclass-based mechanisms throw or silently skip (e.g. a final `@Transactional` method gets no transaction). ## The all-open compiler plugin Rather than forcing developers to sprinkle `open` everywhere, JetBrains ships the **all-open** Gradle/Maven plugin. You configure it with a list of annotations; any class annotated with one becomes implicitly `open` (along with its members) at compile time: ```kotlin plugins { kotlin("plugin.allopen") version "2.0.0" } allOpen { annotation("com.example.OpenForProxy") } ``` ## kotlin-spring preset Most Spring projects use **kotlin-spring**, a thin preset of all-open preconfigured with Spring's stereotype annotations: `@Component`, `@Async`, `@Transactional`, `@Cacheable`, `@SpringBootTest`, and (transitively) `@Service`, `@Repository`, `@Controller`, `@Configuration`. So a `@Service class Foo` is automatically open — no manual keyword needed. ```kotlin plugins { kotlin("plugin.spring") version "2.0.0" } ``` ## Related (different) plugins - **no-arg / kotlin-jpa**: synthesizes a no-arg constructor for `@Entity` classes — a *separate* problem from openness. - **kotlin-jpa** is to no-arg what kotlin-spring is to all-open (a preset). ## Testing without opening Modern mocking avoids the issue rather than opening code: - **MockK** and **Mockito + mockito-inline** mock **final** classes/methods via bytecode instrumentation (the JVM `instrument`/inline mock maker), so you don't need `open` just to test. ## Design judgment Opening a class for a framework is a side effect, not a real extension point. The principal-level view: rely on the plugin for infrastructure annotations, but for your own domain code prefer **interfaces** or selectively `open` only genuine hooks, keeping the public inheritance contract small and the fragile-base-class surface minimal.
- Is kotlin-jpa the same as all-open?No. kotlin-jpa (no-arg plugin) generates a synthetic no-arg constructor for @Entity classes; openness for proxies is handled by all-open / kotlin-spring.
- Can you mock a final Kotlin class without any plugin?Yes — MockK, or Mockito with the inline mock maker, mock final classes/methods via bytecode instrumentation, so no `open` is required.
saying these in an interview costs you the question
- Suggesting you must manually mark every Spring bean `open`
- Confusing all-open with no-arg/kotlin-jpa
- Claiming you can't unit-test final Kotlin classes
- Not knowing why a final @Transactional method silently loses its transaction