Explain why Spring AOP supports execution() but not call(), and what that means for intercepting a method invocation.
answer
- call = caller/call-site; execution = callee/body
- Proxy sits in front -> execution-side only
- No woven caller code -> no call join point
- Self-invocation bypasses proxy
- AspectJ weaving fixes it
basics
~20 scall() 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 sIn 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@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
Just know execution is the one you use; call isn't available.
State that call is caller-side and unsupported, execution is callee-side.
Tie the proxy architecture to why only execution exists and explain the self-invocation consequence and fixes.
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