skip to content

Proxy Limitations & Aspect Ordering

Where proxy-based AOP stops working — self-invocation, final and private methods, method-execution-only join points — plus how multiple aspects are ordered. Nearly every real Spring bug about an annotation that 'does nothing' lives in this box.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

19

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

open as a page

What kinds of join points can proxy-based Spring AOP advise, and what can't it?

level: juniorimportance: must knowfreq 62%

basics

~10 s

Spring AOP only intercepts method executions on Spring beans. It cannot advise field reads/writes, constructors, static methods, or private methods — because it works by wrapping the bean in a proxy object.

open as a page

What is self-invocation in Spring AOP, and why can it make @Transactional silently do nothing?

level: juniorimportance: must knowfreq 78%

basics

~20 s

When a bean calls its own method via this.method(), the call goes straight to the real object, not through Spring's proxy. Because advice like @Transactional lives on the proxy, it is skipped, so no transaction starts.

open as a page

Why does an internal self-invoked call to a @Transactional method sometimes run without a transaction?

level: middleimportance: must knowfreq 70%

basics

~10 s

Because @Transactional is applied by the proxy. When a bean calls its own method with this.method(), the call goes straight to the target and skips the proxy, so the transactional advice never runs.

open as a page

Explain what 'the aspect with the lowest order value has the highest precedence' means for the execution of before advice versus after advice across two aspects.

level: middleimportance: must knowfreq 50%

basics

~20 s

The lowest-value aspect is outermost, like the outer layer of an onion. On the way in, its @Before runs first. On the way out, its @After runs last. So before advice runs highest-precedence-first, and after advice runs highest-precedence-last (reversed).

open as a page

You have a self-invocation bug where an internal call to a @Transactional method isn't transactional. What are your fix options and their trade-offs?

level: seniorimportance: must knowfreq 66%

basics

~20 s

Options: move the method to a separate bean (cleanest), self-inject the bean and call through the injected proxy, use AopContext.currentProxy() with exposeProxy=true, or switch to AspectJ weaving. First is preferred; last removes the limitation entirely.

open as a page

You have two Spring aspects that both match the same method call. By default, which one runs first, and how do you control the order?

level: juniorimportance: should knowfreq 55%

basics

~20 s

By default the order is undefined. To make it deterministic, give each aspect a precedence: annotate the aspect class with @Order(n) or implement the Ordered interface. The aspect with the lower number runs first (outermost).

open as a page

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

level: middleimportance: should knowfreq 58%

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.

open as a page

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

level: middleimportance: should knowfreq 40%

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.

open as a page

Explain the proxy mechanism that causes self-invocation to bypass advice. How do JDK vs CGLIB proxies fit in?

level: middleimportance: should knowfreq 60%

basics

~20 s

Spring wraps your bean in a proxy object that runs advice before delegating to the real target. Other beans get the proxy; but inside the bean, this is the raw target, so its own calls skip the proxy and its advice.

open as a page

A teammate reports that @Cacheable on a service method does nothing. Walk through how you'd diagnose whether a method-visibility/proxy limitation is the cause.

level: seniorimportance: should knowfreq 50%

basics

~20 s

Check whether the method is public and non-final, whether it's called from inside the same bean (self-invocation bypasses the proxy), and whether the bean is actually a proxy. Private/final/static methods or internal calls silence @Cacheable with no error.

open as a page

AspectJ distinguishes execution() from call() and offers get()/set()/initialization(). Which of these does Spring AOP actually implement, and why?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Spring AOP implements only method execution. It does not support call, get/set (fields), initialization (constructors), staticinitialization, or handler, because a runtime proxy can only intercept a method being executed on the wrapped bean.

open as a page

A teammate annotates individual @Before methods with @Order to sequence them and is confused when nothing changes. What's going on, and what would you tell them?

level: seniorimportance: should knowfreq 25%

basics

~20 s

@Order controls precedence between aspect beans, not between advice methods. On a method it's ignored for AOP ordering. To sequence steps, put them in separate aspect classes and order those, or combine them into one advice method.

open as a page

Two advice methods declared in the same @Aspect class both apply to one join point. What determines their relative order, and how has Spring's behaviour here changed?

level: seniorimportance: should knowfreq 35%

basics

~20 s

You cannot order two advice methods within the same aspect via @Order — it only works between aspects. Historically the order was undefined (reflection can't recover source order). Since Spring 5.2.7 they're ordered by advice type: @Around, @Before, @After, @AfterReturning, @AfterThrowing.

open as a page

How does exposeProxy / AopContext.currentProxy() actually work, and when is it the right tool?

level: seniorimportance: should knowfreq 42%

basics

~10 s

With exposeProxy=true, the proxy stores itself in a thread-local before delegating to the target. Inside the method you call AopContext.currentProxy() to get that proxy and invoke the annotated method through it, so advice runs.

open as a page

You need to audit every write to a specific field across domain objects, including objects created with `new`. How do you architect this given Spring AOP's limitations?

level: principalimportance: should knowfreq 30%

basics

~10 s

Spring AOP can't do it — it only advises bean method executions, not field writes or non-Spring objects. Use full AspectJ with a set() pointcut, woven at compile-time (ajc) or load-time (spring-instrument agent).

open as a page

When proxy-based Spring AOP can't advise the methods you need (final/private/static, self-invocation, final classes), what are your options and how do you choose?

level: principalimportance: nice to knowfreq 34%

basics

~20 s

Options: refactor the code to expose a public non-final method through the proxy (e.g. split into a separate bean), or switch from proxy-based Spring AOP to AspectJ weaving, which rewrites bytecode and can advise final/private/static methods and final classes.

open as a page

How would you reason about ordering a custom auditing or security aspect relative to Spring's own transaction management on the same service method?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Decide which concern must bracket the others and give it the lowest order value so it's outermost. Spring's transaction advice has its own configurable order (default lowest precedence / Integer.MAX_VALUE), so an aspect with a lower value runs outside the transaction; a higher value runs inside it.

open as a page

Architecturally, when would you move from Spring proxy AOP to AspectJ weaving to solve self-invocation, and what are the costs?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Switch to AspectJ when you need advice to fire on internal calls (and even private/final methods) across the codebase, not just one spot. Costs: compile-time or load-time weaving setup, a JVM agent for LTW, harder debugging, and broader coupling to AspectJ.

open as a page