Why doesn't Spring AOP advice fire on final, private, or static methods when using CGLIB proxies?
answer
- override is the only hook
- final method: can't override
- private: not inherited, static dispatch
- static: no instance dispatch
- silent skip — no error
basics
~10 sCGLIB works by subclassing your class and overriding methods. Java doesn't let a subclass override final, private, or static methods, so those methods can't be intercepted and their advice is silently skipped.
solid answer
~50 sA CGLIB proxy is a runtime-generated subclass that adds advice by overriding each method to route through an interceptor. Overriding is the only hook, so anything that can't be overridden can't be advised: a final method can't be overridden, a private method isn't inherited/visible to the subclass, and a static method belongs to the class rather than an instance so there's no virtual dispatch to intercept. Additionally, a final class can't be subclassed at all, so it can't be proxied by CGLIB — you'd get a proxy-creation failure. The dangerous part is that missed advice on final/private/static methods is silent: no error, the code just runs without the transaction, cache, or aspect. The fix is to make advised methods public (or at least non-final, non-static) and to avoid marking classes final if they need proxying.
code
java · 19 lines@Service
public class ReportService {
// WON'T be advised: final can't be overridden by the CGLIB subclass
@Transactional
public final void generate() { /* runs WITHOUT a transaction, silently */ }
// WON'T be advised: private isn't overridable
@Cacheable("x")
private String helper() { return "..."; } // cache never applied
// WON'T be advised: static has no instance dispatch
@Async
public static void kickOff() { /* runs synchronously */ }
// OK: public, non-final, non-static → overridable → advised
@Transactional
public void save() { /* wrapped in a transaction */ }
}go deeper
Know the three keywords (final/private/static) plus final class break CGLIB proxying.
Explain WHY each fails via overriding/dispatch and that the skip is silent.
Connect to Kotlin all-open, AspectJ alternatives, and debugging 'advice not firing'.
Reason about API-design conventions (keep advised methods public/non-final) and weaving-strategy tradeoffs at the codebase level.
**The mechanism that requires overriding.** CGLIB adds behavior by generating `class YourType$$SpringCGLIB$$0 extends YourType`. For each method it wants to advise, it emits an override that calls a `MethodInterceptor` (the advice chain) and then, if appropriate, `super.method(...)`. Interception is therefore fundamentally tied to Java method **overriding** and **virtual dispatch**. If a method can't be overridden, there's no place to insert the interceptor. **Case-by-case:** - **`final` method** — the Java language forbids a subclass from overriding it. CGLIB simply can't generate an override, so calls hit the original body directly with no advice. No exception is thrown; the advice is just absent. - **`private` method** — private methods are not inherited and are not part of the subclass's visible API; they use static (non-virtual) dispatch. The subclass can't override them, and even internal calls to them resolve to the original body. So they can't be advised. - **`static` method** — belongs to the class, not to any instance. There is no polymorphic dispatch through an object reference, and CGLIB proxies are *instances* of a subclass. Static methods therefore have no interception point. - **`final` class** — cannot be subclassed at all. CGLIB cannot generate a subclass, so Spring cannot create the proxy. Depending on configuration this surfaces as a proxy-creation error at startup (or Spring falls back / fails, rather than silently producing an unadvised bean). **Why 'silent' is the real trap.** For final/private/static *methods* on an otherwise-proxyable class, Spring successfully builds the proxy for the other methods; the un-overridable ones simply pass through unadvised. So `@Transactional` on a `final` or `private` method compiles, starts, and runs — but without a transaction. This is a classic source of 'my transaction/cache isn't working' bugs. **Related trap — self-invocation.** Even a perfectly overridable public method won't be advised if it's called from *within the same object* (`this.other()`), because that call doesn't go through the proxy. This is a separate issue from final/private/static but often confused with it. **Kotlin note.** In Kotlin, classes and methods are `final` by default. That's why Spring Boot Kotlin projects use the `kotlin-spring` (all-open) compiler plugin, which automatically makes `@Component`/`@Transactional`/etc. classes and their methods open so CGLIB can subclass them. **How to avoid the problem:** - Make advised methods `public` (or at least non-final, non-private, non-static). - Don't mark a class `final` if it needs proxying (or rely on `kotlin-spring` in Kotlin). - Prefer interface-based design + JDK proxies when it fits, which sidesteps subclassing entirely (though the final/self-invocation issues for *method visibility* still apply differently).
- Does Spring throw an error if you put @Transactional on a final method?No — with CGLIB it's silently ignored (the method runs without a transaction). That silent failure is exactly why it's dangerous. AspectJ compile/load-time weaving could handle it, but standard Spring AOP can't.
- How do Kotlin Spring projects deal with final-by-default?The kotlin-spring (all-open) compiler plugin marks Spring-annotated classes and methods as open, letting CGLIB subclass and override them.
saying these in an interview costs you the question
- Claiming Spring throws a clear error for @Transactional on a final/private method (it's silently skipped)
- Saying private methods can be proxied if you make the interceptor reflective
- Confusing this with self-invocation as the only cause