skip to content

Why might @Before advice silently not fire, and how do proxy mechanics explain it?

level: seniorimportance: must knowfreq 50%

answer

  1. Proxy-based: advice only on calls through the proxy
  2. Self-invocation this.foo() bypasses — silent
  3. private/final/static/non-bean not advised
  4. Need @EnableAspectJAutoProxy / starter-aop
  5. Fix: split beans / self-inject / AopContext.currentProxy()

basics

~20 s

Spring AOP is proxy-based, so @Before only fires when the method is called through the proxy from another bean. Self-invocation (this.method()), private/final methods, non-bean calls, or missing @EnableAspectJAutoProxy all cause it to silently not run.

solid answer

~50 s

Spring AOP weaves advice via a runtime proxy (JDK dynamic proxy or CGLIB subclass) that wraps the bean. Advice like @Before only executes when the call goes *through* that proxy. The classic failure is self-invocation: when a bean calls its own advised method via this.foo(), the call bypasses the proxy and no advice fires — because this is the raw target, not the proxy. Other causes: the target isn't a Spring bean; the method is private (never proxied) or, under CGLIB, final (can't be overridden); AOP auto-proxying isn't enabled (@EnableAspectJAutoProxy or spring-boot-starter-aop); or the pointcut simply doesn't match. Fixes for self-invocation: split the method into a second bean, inject a self-reference/AopContext.currentProxy(), or restructure so the entry point is called externally. It fails silently — no error — which is why it's a common interview gotcha.

code

java · 14 lines
java
@Service
public class ReportService {

    // Injecting the bean into itself gives the PROXY, not the raw target
    private final ObjectProvider<ReportService> self;
    public ReportService(ObjectProvider<ReportService> self) { this.self = self; }

    public void run() {
        // this.generate() would bypass the proxy -> @Before would NOT fire
        self.getObject().generate();   // goes through proxy -> advice fires
    }

    public void generate() { /* advised by some @Before aspect */ }
}

go deeper

for a junior

Know AOP is proxy-based and that internal calls can skip advice.

for a middle

Explain self-invocation concretely and that it fails silently.

for a senior

Enumerate all proxy limitations (private/final/static/non-bean, enablement) and the standard fixes.

for a principal

Connect to @Transactional/@Cacheable/@Async, weigh proxy vs AspectJ weaving tradeoffs, and set patterns that avoid self-invocation by design.

## Spring AOP is proxy-based Spring AOP does **not** modify your class bytecode (that's full AspectJ weaving). Instead, at bean-creation time Spring wraps the target bean in a **proxy** object: - **JDK dynamic proxy** if the bean implements interfaces (proxies the interface). - **CGLIB proxy** (a runtime subclass) otherwise — the default in Spring Boot for concrete classes. Other beans get the **proxy** injected, not the raw target. The proxy intercepts each incoming method call, runs the matching advice (e.g. `@Before`), then delegates to the real target. **Advice fires only on calls that pass through the proxy.** ## The #1 cause: self-invocation When a method inside the bean calls another method on the **same** instance using an implicit or explicit `this`, the call does **not** go through the proxy — it's a direct in-object call. So advice on the called method **does not run**: ```java @Service public class ReportService { public void run() { generate(); // == this.generate(); bypasses the proxy -> @Before does NOT fire } @Timed // some @Before advice matches generate() public void generate() { /* ... */ } } ``` Called from *outside* (another bean invoking `reportService.generate()`), the advice fires. Called *internally* via `run()`, it doesn't. This silent inconsistency is the classic gotcha. ### Fixes for self-invocation 1. **Refactor into two beans** — move `generate()` into a separate bean and inject it; the cross-bean call goes through that bean's proxy. (Cleanest.) 2. **Inject a self-reference** — inject the bean into itself (Spring resolves the proxy) and call `self.generate()`. 3. **`AopContext.currentProxy()`** — enable `@EnableAspectJAutoProxy(exposeProxy = true)` and call `((ReportService) AopContext.currentProxy()).generate()`. Works but couples code to Spring AOP. 4. **Switch to compile-/load-time AspectJ weaving** — real weaving instruments the bytecode, so even self-calls are advised. Heavier setup. ## Other reasons @Before silently doesn't fire - **Not a Spring bean** — AOP only advises beans in the context. A `new`-ed object is never proxied. - **`private` methods** — never proxied (not visible to override/interface). Advice can't apply. - **`final` methods/classes under CGLIB** — CGLIB subclasses the target; `final` can't be overridden, so those methods aren't advised (and a `final` class can't be proxied by CGLIB at all). - **`static` methods** — not instance calls; not proxyable. - **AOP not enabled** — need `@EnableAspectJAutoProxy` on a `@Configuration` class, or simply `spring-boot-starter-aop` on the classpath (Boot autoconfigures it). Without it, aspects are inert. - **Aspect not a bean** — the `@Aspect` class must also be a Spring bean (e.g. `@Component`) or declared as `@Bean`. - **Pointcut doesn't match** — wrong `execution(...)` expression, wrong package, visibility, or return/arg types. (Spring AOP `execution` effectively matches public methods only.) - **Wrong reference injected** — using the raw target obtained via reflection or `getTargetObject()` bypasses the proxy. ## How to diagnose - Confirm `spring-boot-starter-aop`/`@EnableAspectJAutoProxy` present. - Check the call path: is the advised method entered from *another* bean, not internally? - Verify method visibility (public, non-final, non-static) and that the class is a managed bean. - Temporarily set logging on `org.springframework.aop` / enable proxy-target-class and inspect whether the injected bean is a `$$EnhancerBySpringCGLIB` / `$Proxy` instance. ## Why it matters at senior level The failure is **silent** — no exception, the method just runs un-advised, so logging/security/transactions quietly don't apply. `@Transactional`, `@Cacheable`, `@Async` are all proxy-based and hit the **same** self-invocation trap; understanding it for `@Before` transfers directly to those.

  • Name three other Spring features that share this self-invocation limitation.
    @Transactional, @Cacheable, and @Async — all are proxy-based, so an internal this.method() call bypasses them just like @Before.
  • How does full AspectJ (compile/load-time weaving) avoid the self-invocation problem?
    It instruments the actual bytecode rather than wrapping with a proxy, so even calls made through this are advised. It needs a weaving agent or build-time weaving setup.

saying these in an interview costs you the question

  • Assuming advice always fires regardless of how the method is called
  • Thinking private or final methods can be advised by Spring AOP proxies
  • Not knowing that @Transactional/@Cacheable share the same trap

context