skip to content

Why can't CGLIB proxies advise final classes or final/static methods? Explain the underlying mechanism.

level: middleimportance: should knowfreq 58%

answer

  1. CGLIB = runtime subclass + method override
  2. Bound by Java inheritance rules
  3. final class → can't subclass → startup fails
  4. final/static/private → can't override → silent skip
  5. Kotlin final-by-default trap

basics

~20 s

CGLIB creates a runtime subclass and overrides methods to insert advice. A final class can't be subclassed, and final/static methods can't be overridden, so there's nothing for CGLIB to hook into — those methods and classes can't be proxied.

solid answer

~50 s

CGLIB implements proxying by generating a **subclass** of your target class at runtime. Each advised method is overridden in that subclass; the override runs the interceptor chain, then calls `super`. This is pure Java inheritance, so it inherits Java's overriding rules. A `final` class cannot be subclassed at all, so CGLIB can't create a proxy — Spring startup fails or falls back. A `final` method cannot be overridden, so CGLIB emits the subclass but leaves that method as-is: it runs with no advice. A `static` method belongs to the class, is resolved at compile time and isn't polymorphic, so a subclass override is impossible — again no advice. `private` methods are invisible to the subclass, same result. The takeaway: CGLIB can only intercept what standard Java overriding allows — non-final, non-static, non-private methods on a non-final class.

code

kotlin · 14 lines
kotlin
// Kotlin classes & methods are FINAL by default — a common CGLIB trap.
// Without the kotlin-spring / all-open plugin, CGLIB cannot subclass this,
// so @Transactional advice is never applied.
@Service
class PaymentService(private val repo: PaymentRepository) {

    // Implicitly final in Kotlin -> not overridable -> no proxy advice
    @Transactional
    fun charge(id: Long) { /* runs WITHOUT a transaction unless made open */ }
}

// Fix: apply the kotlin-spring Gradle plugin (adds all-open for @Service etc.)
// or declare the class/method `open`:
// @Service open class PaymentService { @Transactional open fun charge(id: Long) { } }

go deeper

for a junior

Enough to say CGLIB subclasses the class and overrides methods, so final/static/private don't work.

for a middle

Should articulate the inheritance/override mechanism and the final-class startup failure vs final-method silent skip distinction.

for a senior

Should bring in JDK-vs-CGLIB selection, Spring Boot's proxyTargetClass default, and the Kotlin final-by-default gotcha.

for a principal

Should weigh proxy limitations vs AspectJ weaving as an architectural choice, plus team-wide conventions to prevent silent misconfiguration.

## What CGLIB actually does CGLIB (Code Generation Library, bundled inside spring-core) creates proxies by **generating a new class at runtime that extends your target class**. Conceptually: ``` class OrderService$$SpringCGLIB$$0 extends OrderService { @Override public void placeOrder(Order o) { // run the advice / interceptor chain // then super.placeOrder(o) } } ``` Spring registers a `MethodInterceptor` (via CGLIB's `Enhancer`/`Callback` mechanism) so each overridden method routes through the AOP interceptor chain before invoking the original via a super-call. **Everything hinges on method overriding**, which is ordinary Java inheritance — so CGLIB is bound by the JVM's inheritance and overriding rules. ## Why each case fails ### final class The JVM forbids extending a `final` class. `Enhancer` cannot generate a subclass, so **no proxy can be created at all**. If such a bean requires a CGLIB proxy, you get a startup failure (e.g. a `BeanCreationException` / "Cannot subclass final class") — Spring cannot fall back to JDK proxies unless the class implements interfaces. ### final method The JVM forbids overriding a `final` method. CGLIB still generates the subclass, but that particular method is **left inherited unchanged**. Calls execute the original directly with **no advice**. No error — silent skip. ### static method `static` methods are dispatched by the **compile-time type**, not by the runtime instance (no virtual dispatch). A subclass declaring a same-signature static method only *hides* it, it doesn't *override* it, and instance-based proxying never routes through it. So static methods are **never advised**. ### private method `private` methods are not inherited and not visible to the subclass, so they cannot be overridden. Additionally they are always called via `this`, which points at the real object during a super-call — bypassing the proxy. **No advice.** ## JDK proxies for contrast JDK dynamic proxies don't subclass; they implement your **interfaces**. They can only advise methods **declared on an interface** (all `public`). They are immune to "final class" (they don't subclass) but limited to interface methods. Spring Boot defaults to CGLIB (`spring.aop.proxy-target-class=true`) precisely so proxies work on concrete classes without interfaces. ## The escape hatch: AspectJ AspectJ doesn't use proxies. It **weaves** advice directly into bytecode at compile time (`ajc`) or class-load time (LTW agent). Because it rewrites the method body itself, it can advise `final`, `private`, and `static` methods and works on `final` classes and constructors. Choose AspectJ when proxy limitations are genuinely blocking; otherwise Spring AOP's proxying is simpler. ## Practical guidance - Don't mark advised beans/methods `final`; watch out for Kotlin, where classes/methods are `final` by default (use `all-open`/`kotlin-spring` plugin). - Never rely on advice on `static`/`private` methods. - If a `final` class must be proxied, extract an interface (enabling JDK proxies) or drop `final`.

  • What happens at application startup if a bean that needs a CGLIB proxy is a final class with no interfaces?
    Proxy creation fails — CGLIB can't subclass a final class — and Spring can't fall back to a JDK proxy without interfaces, so you get a BeanCreationException / 'Cannot subclass final class' at startup.
  • Why is this a bigger issue in Kotlin than in Java?
    Kotlin classes and members are final by default, so beans are non-proxyable out of the box. The kotlin-spring plugin (all-open) automatically opens Spring-annotated classes/methods to fix this.

context