skip to content

Explain why Spring AOP supports execution() but not call(), and what that means for intercepting a method invocation.

level: seniorimportance: should knowfreq 40%

answer

  1. call = caller/call-site; execution = callee/body
  2. Proxy sits in front -> execution-side only
  3. No woven caller code -> no call join point
  4. Self-invocation bypasses proxy
  5. AspectJ weaving fixes it

basics

~20 s

call() matches at the caller's side; execution() matches where the method actually runs (callee side). Spring's proxy sits in front of the target, so it can only observe the execution. There's no caller-side hook, hence no call().

solid answer

~50 s

In AspectJ, `call(...)` and `execution(...)` are two different join points for the same method. `call` fires at the **call site** — inside the caller, before/after the dispatch — and can see the caller's context. `execution` fires at the **method body** — on the callee side, once dispatch has landed on the target. Spring AOP is proxy-based: the proxy stands between caller and target and can only intercept the invocation as it arrives at the target and delegates inward. That is inherently execution-side. There is no bytecode woven into the caller, so a caller-side join point can't exist — `call` is unsupported and throws IllegalArgumentException. The practical fallout: two beans in the same class calling each other (self-invocation) never cross the proxy boundary, so no advice runs; only calls that arrive through the injected proxy reference are advised.

code

java · 15 lines
java
@Service
public class ReportService {

    private final ReportService self; // self-inject to re-enter the proxy

    public ReportService(@Lazy ReportService self) { this.self = self; }

    public void run() {
        generate();       // NOT advised — bypasses proxy (execution-only limit)
        self.generate();  // advised — goes through the proxy
    }

    @Cacheable("reports")
    public String generate() { return heavyWork(); }
}

go deeper

for a junior

Just know execution is the one you use; call isn't available.

for a middle

State that call is caller-side and unsupported, execution is callee-side.

for a senior

Tie the proxy architecture to why only execution exists and explain the self-invocation consequence and fixes.

for a principal

Weigh restructuring vs AspectJ weaving as the systemic fix and its runtime/build-time trade-offs.

## call vs execution: two join points, one method ### The AspectJ model For a single method invocation, AspectJ recognizes **two** distinct join points: - **call join point**: located in the **caller's** code, at the point of dispatch. Woven into the *caller* bytecode. Has access to caller-side static context and can advise the invocation even before polymorphic dispatch resolves the actual target. - **execution join point**: located in the **callee's** method body. Woven into the *target* bytecode. Runs once, wherever the method is actually executed, regardless of who called it. ### Why Spring AOP only has execution Spring AOP does **not weave bytecode**. It creates a **proxy** object: - **JDK dynamic proxy** when the bean implements interfaces — an `InvocationHandler` receives every interface-method call. - **CGLIB proxy** otherwise — a generated subclass overrides methods and routes through a `MethodInterceptor`. Either way, the interception point is 'a call has *arrived* at the proxy and is about to be delegated to the real target.' That is precisely the **execution** semantics (from the target's perspective). Because nothing is inserted into the *caller's* code, there is no place for a **call** join point to live. So `call` is rejected with `IllegalArgumentException` at startup. ### The concrete, high-impact consequence: self-invocation Since interception only happens when a call **passes through the proxy**, an internal call bypasses advice: ```java @Service public class OrderService { @Transactional public void outer() { inner(); } // 'this.inner()' — NOT proxied @Transactional(propagation = REQUIRES_NEW) public void inner() { /* new tx? NO */ } } ``` `outer()` calls `this.inner()`, which goes to the raw object, not the proxy — so `inner()`'s transactional advice never applies. With AspectJ `call`/weaving this would be intercepted; with Spring AOP it is not. ### Workarounds when you hit the limit 1. **Inject a self-reference** and call through it (`self.inner()`), so the call re-enters the proxy. 2. **`AopContext.currentProxy()`** with `exposeProxy = true` on `@EnableAspectJAutoProxy`. 3. **Restructure** — move `inner()` into a separate bean. 4. **Switch to AspectJ weaving** (load-time or compile-time), which supports `call` and weaves the target regardless of how it's invoked, eliminating the self-invocation blind spot. ### Interview-grade summary - `call` = caller side, `execution` = callee side. - Proxy = execution-side only; `call` unsupported. - The tangible symptom of 'no call join point' is the self-invocation trap that bites `@Transactional`, `@Cacheable`, `@Async`, and custom aspects alike.

  • How would you make an internal call actually trigger @Cacheable/@Transactional advice?
    Route the call through the proxy: self-inject the bean and call self.method(), use AopContext.currentProxy() with exposeProxy=true, move the method to a separate bean, or switch to AspectJ weaving which advises the execution regardless of caller.

saying these in an interview costs you the question

  • Claiming call() and execution() are interchangeable in Spring
  • Saying self-invocation is advised in Spring AOP
  • Thinking Spring weaves the caller's bytecode

context