skip to content

Why do final classes, final/private methods, and static methods escape Spring AOP advice?

level: middleimportance: should knowfreq 40%

answer

  1. JDK proxy = interfaces; CGLIB = subclass
  2. final class → can't subclass (CGLIB fails)
  3. final/private method → can't override
  4. static → no instance to proxy
  5. Kotlin final-by-default → kotlin-spring plugin

basics

~20 s

Spring AOP advises via a proxy that either implements the bean's interfaces (JDK) or subclasses it (CGLIB). Final classes/methods can't be subclassed/overridden, private methods aren't overridable/on interfaces, and static methods have no instance — so none get intercepted.

solid answer

~40 s

All of these stem from the proxy mechanism. A **JDK dynamic proxy** implements the target's interfaces and can only advise interface methods. A **CGLIB proxy** subclasses the target and overrides its methods — so it needs methods it *can* override. A **`final` class** can't be subclassed at all (CGLIB fails), and a **`final` method** can't be overridden, so it runs directly on the target without advice. A **`private` method** is neither on any interface nor overridable, so the proxy never sees it. A **`static` method** belongs to the class, not an instance, and the proxy wraps an instance — nothing to intercept. In all cases the call reaches the real member directly, silently skipping advice. This is another face of the 'method-execution-on-a-proxied-bean only' limitation; AspectJ weaving (which edits bytecode) can reach all of them.

code

kotlin · 16 lines
kotlin
@Service
open class ReportService {   // 'open' so CGLIB can subclass (or use kotlin-spring plugin)

    @Transactional
    open fun generate() {        // 'open' + public -> advisable
        // ...
    }

    // final by default in Kotlin -> NOT advisable even if @Transactional:
    @Transactional
    fun generateFinal() { }      // silently no transaction

    // private -> never intercepted by proxy AOP
    @Transactional
    private fun helper() { }
}

go deeper

for a junior

Know that private/static/final methods aren't advised.

for a middle

Explain it via JDK-interface vs CGLIB-subclass proxy mechanics.

for a senior

Connect to Kotlin final-by-default and the kotlin-spring plugin; know AspectJ escapes these limits.

for a principal

Fold into a design checklist for reliable proxying and decide when a case justifies AspectJ.

## Two proxy strategies, one common constraint Spring AOP wraps a bean in a **proxy**: - **JDK dynamic proxy** — created when the bean implements at least one interface. The proxy implements those **interfaces** and routes each interface method through the advice chain. It can advise **only** methods declared on an interface. - **CGLIB proxy** — created when there's no interface (or `proxyTargetClass=true`). CGLIB generates a **subclass** of the target at runtime and **overrides** its methods to insert advice. Both strategies intercept by standing in for **overridable, instance-level, externally-dispatched methods**. Anything that isn't overridable/dispatchable that way escapes: ### `final` class CGLIB works by subclassing. A `final` class **cannot be subclassed**, so CGLIB proxy creation **fails** (you'll get an error, or with interfaces Spring falls back to JDK proxy which only sees interface methods). Kotlin gotcha: Kotlin classes and methods are `final` by default — that's why Spring Boot's Kotlin support uses the **`kotlin-spring`** (all-open) compiler plugin to make `@Component`/`@Configuration`/etc. classes open so CGLIB can subclass them. ### `final` method Even on a non-final class, a `final` method **can't be overridden**, so CGLIB can't wrap it. Calls run straight on the target — no advice. ### `private` method A `private` method is not part of any interface and can't be overridden by a subclass. Neither proxy strategy can intercept it. (Spring's `@Transactional`/`@Cacheable` also ignore non-public methods by default.) ### `static` method Static methods belong to the **class**, not an instance. A proxy wraps an **instance**; there's no instance dispatch to intercept, so statics are never advised. ## The unifying rule Spring AOP advises the **execution of an overridable instance method reached through the proxy**. `final`, `private`, and `static` all break 'overridable instance method reached through the proxy', so advice is silently skipped — a frequent source of 'my aspect isn't firing' confusion. ## How to avoid the trap - Make advised methods **public** and **non-final**; keep the class non-final (in Kotlin, rely on `kotlin-spring` or mark `open`). - Don't put cross-cutting behavior on static utility methods; put it on bean instance methods. - If you truly must advise a static/final/private member, use **AspectJ weaving** (CTW or LTW), which rewrites bytecode and isn't bound by override rules. ## Quick diagnostic If an aspect/`@Transactional`/`@Cacheable` 'does nothing', check in order: is it a **public**, **non-final**, **instance** method, invoked from **another bean** (not self-invocation), on a **non-final** class? Most silent AOP failures are one of these.

  • Why does Spring Boot need the kotlin-spring compiler plugin?
    Kotlin classes and methods are final by default. CGLIB proxies need to subclass and override, so it can't proxy final classes/methods. The kotlin-spring (all-open) plugin automatically makes classes annotated with @Component, @Configuration, @Transactional, etc. open, allowing proxy creation.

saying these in an interview costs you the question

  • Saying CGLIB can proxy final classes if you enable proxyTargetClass
  • Believing static methods can be advised with @Aspect
  • Not realizing Kotlin's final-by-default breaks proxying without kotlin-spring

context