Explain advice ordering around a failure and the proxy limitations that determine whether @AfterThrowing fires at all.
answer
- Unwind inner→outer on throw
- @Order sets outer/inner; outermost fires last
- Self-invocation bypasses proxy
- final/static/private → not advisable
- Swallowing @Around hides it from outer @AfterThrowing
basics
~20 sOn a throw, inner advice unwinds first: @AfterThrowing and @After run as the exception propagates back out through the proxy chain. But @AfterThrowing only fires for calls that go through the proxy — self-invocation, private/final/static methods, and swallowing @Around advice can prevent it.
solid answer
~50 sSpring builds an interceptor chain per advised method; on a throw the exception unwinds it inner-to-outer. For a single aspect, @AfterThrowing and @After both run on the exceptional path (@After like a finally); across multiple aspects, @Order/Ordered controls nesting — the highest-precedence aspect is outermost, so on the way out its after-throwing advice runs last. Whether @AfterThrowing fires at all depends on the proxy: Spring AOP only intercepts external calls through the proxy. Self-invocation (this.other()) bypasses it; private, final, and static methods can't be advised (JDK proxies need interface methods, CGLIB can't override final); and a wrapping @Around that catches the exception and returns normally means no exception propagates outward, so an outer @AfterThrowing never sees it. Also, if the target throws during construction or the pointcut doesn't match, no advice runs. These are the classic why-didn't-my-aspect-fire causes.
code
java · 17 lines@Aspect @Order(1) @Component
class OuterAspect {
@AfterThrowing(pointcut = "execution(* svc..*(..))", throwing = "ex")
void outer(Throwable ex) { /* runs LAST on unwind (outermost) */ }
}
@Aspect @Order(2) @Component
class InnerAspect {
@AfterThrowing(pointcut = "execution(* svc..*(..))", throwing = "ex")
void inner(Throwable ex) { /* runs FIRST on unwind (closest to target) */ }
}
// Self-invocation gotcha: b() bypasses the proxy, so its advice won't fire
class Svc {
public void a() { b(); } // internal call — no proxy
public void b() { throw new IllegalStateException(); }
}go deeper
Aware advice runs on the throw path.
Knows self-invocation and visibility limits.
Explains @Order precedence and @Around swallowing interaction.
Designs ordered observability/tx stacks and reasons about full unwind semantics and proxy constraints.
### 1. The interceptor chain and unwinding order When a method is advised, Spring composes a **chain of `MethodInterceptor`s** (each advice becomes one). A call runs *down* the chain to the target; on a throw the stack **unwinds inner-to-outer**. So the advice physically closest to the target reacts to the exception first, and the outermost reacts last. ### 2. Ordering within one aspect For a single aspect touching one join point, on the **exceptional** path both `@AfterThrowing` and `@After` run (`@After` is *finally*-style, runs on success and failure). `@AfterReturning` does **not** run. In modern Spring (5.2.7+) the after-advice ordering within an aspect was made deterministic so that on the throwing path after-advice runs in reverse of before-advice. ### 3. Ordering across multiple aspects When several aspects match the same join point, precedence is set by `@Order` / implementing `Ordered` (lower value = higher precedence). The highest-precedence aspect is the **outermost** wrapper. On entry its `@Before` runs first; on a throw its `@AfterThrowing`/`@After` run **last** (outermost unwinds last). Without explicit ordering, the order across aspects is **undefined** — a real gotcha for things like transaction + logging aspects. ### 4. Interaction with @Around An `@Around` sits in the chain like any interceptor. If an inner/outer `@Around` **catches** the exception and returns normally, no exception propagates past it, so any `@AfterThrowing` **outside** that point never fires — the failure was neutralized. Conversely if `@Around` rethrows, downstream after-throwing advice sees it. ### 5. Proxy limitations — the real reasons it doesn't fire Spring AOP is **proxy-based**, and that constrains what can be advised: - **Self-invocation**: if a method calls another advised method on `this`, the call does not go through the proxy, so its advice (including `@AfterThrowing`) is bypassed. Fix: inject the bean into itself, use `AopContext.currentProxy()`, or refactor. - **Method visibility/kind**: JDK dynamic proxies only advise **public interface methods**; CGLIB subclass proxies advise public/protected methods but **cannot override `final` or `static`** methods, and cannot proxy `private` methods. Such methods silently miss advice. - **final class / no default constructor**: CGLIB can't subclass a `final` class. - **Bean must be Spring-managed**: `new`-ing the object yourself yields no proxy, no advice. - **Pointcut mismatch / wrong bean**: obvious but common — the expression doesn't select the executed method. ### 6. Other subtleties - Exceptions thrown **before** the target method entry (e.g. by an earlier interceptor) may be handled differently by outer advice. - `@AfterThrowing` binding uses the **declared parameter type** to filter; a too-narrow type means it won't fire even though an exception occurred. - Async/scheduled boundaries (`@Async`) move execution to another thread — exceptions there don't propagate to the original caller, changing what after-throwing sees. ### When this matters Designing an observability/transaction stack: you must set `@Order` deliberately so, e.g., the transaction aspect wraps the logging aspect, and you must ensure the methods you care about are actually proxied (public, called externally).
- Two aspects both have @AfterThrowing on the same method with no @Order. Which runs first?Undefined. Without @Order/Ordered the cross-aspect order is unspecified; you must set precedence explicitly to make it deterministic.
- Why might @AfterThrowing not fire even though the method clearly threw?Common causes: the call was a self-invocation (bypassed the proxy); the method is private/final/static or the bean wasn't Spring-managed; a wrapping @Around swallowed the exception; the throwing parameter type is too narrow; or the pointcut doesn't match.
- How can you make self-invoked calls get advised?Inject the bean into itself and call through that reference, use AopContext.currentProxy() (with exposeProxy=true), or refactor the inner method into a separate bean.
saying these in an interview costs you the question
- Assuming multiple aspects run in a deterministic order without @Order
- Thinking @AfterThrowing fires on internal self-invocations
- Believing final/static/private methods can be advised by Spring AOP proxies
- Assuming outermost aspect's after-throwing runs first rather than last