skip to content

Which methods can Spring AOP proxies actually intercept, and which are silently skipped?

level: juniorimportance: must knowfreq 72%

answer

  1. Proxy must override → only overridable methods
  2. public/protected, non-final, instance
  3. final/private/static = no advice
  4. Fails silently — compiles, runs, no effect
  5. AspectJ has no such limit

basics

~10 s

Spring AOP can only intercept public or protected, non-final instance methods. Final methods, private methods, and static methods are not advised, so annotations like @Transactional or @Async on them silently do nothing.

solid answer

~40 s

Spring AOP works by wrapping your bean in a proxy object. For that proxy to run advice (like a transaction or @Async), it must be able to override your method. That means only non-final instance methods that are visible to the proxy can be intercepted. With CGLIB (subclass proxies) that's public and protected non-final methods; with JDK dynamic proxies it's public methods declared on an interface. final methods can't be overridden, private methods aren't visible to a subclass, and static methods aren't polymorphic — so none of them get advice. The dangerous part is it fails silently: putting @Transactional on a private or final method compiles and runs, but the advice never fires. Keep advised methods public/protected and non-final, and call them through the proxy, not via 'this'.

code

java · 19 lines
java
@Service
public class OrderService {

    // WORKS: public, non-final, instance method → proxy can override it
    @Transactional
    public void placeOrder(Order o) { /* real transaction */ }

    // BROKEN: private → not overridable, advice silently skipped, NO transaction
    @Transactional
    private void auditOrder(Order o) { /* runs with no tx */ }

    // BROKEN: final → JVM forbids override, advice silently skipped
    @Transactional
    public final void cancelOrder(Order o) { /* runs with no tx */ }

    // BROKEN: static → not polymorphic, never proxied
    @Transactional
    public static void reindex() { /* runs with no tx */ }
}

go deeper

for a junior

Must know the one-liner: only public/protected, non-final, non-static instance methods get advice; final/private/static are silently skipped.

for a middle

Should explain WHY (proxy overriding) and connect it to real annotations like @Transactional/@Async failing silently.

for a senior

Should distinguish JDK vs CGLIB visibility rules and mention self-invocation as a related but separate trap.

for a principal

Should reason about detection (BeanFactory warnings), team conventions/linting, and when to escalate to AspectJ weaving.

## The core mechanism Spring AOP is **proxy-based**. When a bean has advice attached (via `@Transactional`, `@Async`, `@Cacheable`, `@Retryable`, a custom `@Aspect`, etc.), Spring does not modify your class's bytecode. Instead it hands you a **proxy** — a separate object that wraps the real bean. Calls hit the proxy first; the proxy runs the advice (open a transaction, submit to a thread pool, check a cache) and then delegates to your real object. There are two proxy strategies: - **JDK dynamic proxies** — used when the bean implements at least one interface (historically the default). The proxy implements the same interfaces. It can therefore only intercept **methods declared on those interfaces**, which are always `public`. - **CGLIB proxies** — used when there is no interface, or when forced (Spring Boot forces CGLIB by default via `proxyTargetClass=true`). CGLIB generates a **runtime subclass** of your bean and **overrides** each method to weave in advice. Because both strategies rely on **method overriding / interface implementation**, only methods that *can* be overridden or *are* on an interface can be advised. ## What is interceptable **Only `public` or `protected`, non-`final`, non-`static` instance methods** — and for JDK proxies, only `public` interface methods. ## Why each excluded kind is excluded - **`final` methods** — the JVM forbids overriding a `final` method, so the CGLIB subclass cannot wrap it. No advice. - **`private` methods** — not visible to a subclass; a subclass cannot override what it cannot see. No advice. (Private methods are also invoked via `this`, i.e. self-invocation, which bypasses the proxy anyway.) - **`static` methods** — belong to the class, not an instance; they are resolved at compile time and are not polymorphic, so a subclass cannot override them. No advice. - **`final` classes** — CGLIB cannot even create a subclass, so the entire class cannot be proxied by CGLIB; startup can fail or Spring falls back. ## The silent-failure gotcha The worst property is that it **fails silently**. `@Transactional private void save()` compiles, deploys, and runs — but there is **no transaction**. Spring may log a warning at startup for some cases, but there is no compile error and often no runtime error; you just lose the behavior. This is a classic source of "my @Transactional/@Async/@Cacheable isn't working" bugs. ## Practical rules 1. Make advised methods `public` (or at least `protected`) and non-`final`. 2. Don't put AOP annotations on `private`/`static` methods or `final` classes. 3. Avoid self-invocation — calling an advised method via `this.method()` skips the proxy even if the method itself is fine. 4. If you genuinely need to advise private/final/static methods, use **AspectJ** (compile-time or load-time weaving), which rewrites bytecode directly and has none of these proxy limitations.

  • Why does @Transactional on a private method fail without any error?
    Spring only adds the transactional behavior in the proxy, which relies on overriding the method. A private method can't be overridden and is called via 'this', so the proxy is bypassed. There's nothing to error on — the plain method just runs without a transaction.
  • Does making a method protected instead of public let CGLIB advise it?
    Yes. CGLIB subclass proxies can override protected methods, so protected non-final instance methods are interceptable. JDK dynamic proxies, however, only see public interface methods, so protected wouldn't be advised there.

context