Why do internal (self-invocation) method calls in a Spring bean skip @Transactional/@Cacheable advice, and what does setting exposeProxy=true do about it?
answer
- this bypasses the proxy
- advice lives on proxy not target
- ThreadLocal holds current proxy
- cast AopContext.currentProxy()
- default off → IllegalStateException
basics
~20 sSpring advice like @Transactional lives on a proxy that wraps the bean. When one method calls another via this, the call bypasses the proxy, so no advice runs. Setting exposeProxy=true lets a method fetch the proxy and re-route the call so advice still applies.
solid answer
~40 sSpring adds behaviors like @Transactional, @Cacheable and @Async through a proxy that wraps your bean; the advice only fires when a call goes through that proxy. When method A inside the bean calls method B using `this.B()`, the call goes straight to the raw target object, not the proxy, so B's annotations are silently ignored — the self-invocation problem. Setting `@EnableAspectJAutoProxy(exposeProxy = true)` makes Spring publish the current proxy in a ThreadLocal. You then call `((MyService) AopContext.currentProxy()).B()` so the internal call routes back through the proxy and B's advice runs. It's a targeted escape hatch; self-injection or splitting into a second bean are cleaner, and AspectJ weaving avoids the problem entirely.
code
java · 20 lines@Service
public class OrderService {
public void placeOrder(Order o) {
// self-call via `this` would SKIP @Transactional on charge():
// this.charge(o); // <-- advice NOT applied
// route back through the proxy so advice runs:
((OrderService) AopContext.currentProxy()).charge(o);
}
@Transactional
public void charge(Order o) {
// transactional work
}
}
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true) // publishes proxy to AopContext
class AopConfig {}go deeper
Must know that self-calls skip advice and that this is why an internal @Transactional 'does nothing'.
Should know exposeProxy=true + AopContext.currentProxy() as the fix and the cast requirement.
Should weigh it against self-injection / separate bean and know the IllegalStateException failure mode.
Frames it as coupling to AOP internals and prefers architectural fixes or AspectJ weaving; knows the ThreadLocal mechanics.
## The problem Spring implements cross-cutting concerns — `@Transactional`, `@Cacheable`, `@Async`, custom `@Aspect` advice — with **proxy-based AOP**. When you ask the container for a bean, you don't get your class instance directly; you get a **proxy** that wraps the real **target** object. The proxy is either a **JDK dynamic proxy** (when the bean implements an interface) or a **CGLIB subclass** (when it doesn't). Advice is attached to the proxy: an incoming call hits the proxy first, the proxy runs the advice (open a transaction, check the cache…), then delegates to the target. ``` caller ──▶ [Proxy: advice] ──▶ [Target bean: your code] ``` ## Why self-invocation breaks it Inside the target object, `this` refers to the **raw target**, not the proxy. So when method `outer()` calls `this.inner()` (or just `inner()`), the call never leaves the target object — it goes straight from raw method to raw method. The proxy is completely bypassed, so any advice on `inner()` (its `@Transactional`, `@Cacheable`, etc.) **does not run**. This is the classic *self-invocation* / *internal call* gotcha, and it fails silently — no error, the annotation just does nothing. ## What exposeProxy does `exposeProxy=true` tells Spring's AOP framework to **publish the current proxy into a ThreadLocal** for the duration of a proxied invocation. You enable it on the proxy config: - `@EnableAspectJAutoProxy(exposeProxy = true)` for `@Aspect`/`@EnableAspectJAutoProxy` setups - `@EnableTransactionManagement` and `@EnableCaching` don't have the flag directly; but the underlying `AbstractAdvisorAutoProxyCreator`/`ProxyConfig` does. In practice enabling `@EnableAspectJAutoProxy(exposeProxy = true)` sets it globally for the auto-proxy creator, or you set `exposeProxy` on a `ProxyFactoryBean`/advisor. With the flag on, inside a proxied call you can retrieve the proxy: ```java ((MyService) AopContext.currentProxy()).inner(); ``` Now `inner()` is invoked *through the proxy*, so its advice runs. ## Key APIs - **`org.springframework.aop.framework.AopContext`** — utility with a static `currentProxy()` method backed by a `ThreadLocal<Object>`. - **`AopContext.currentProxy()`** — returns the proxy that is currently executing on this thread. Cast it to your bean's type/interface. ## Gotchas - If `exposeProxy` is **false** (the default), `AopContext.currentProxy()` throws `IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true'.` - It only works **inside** a proxied call chain — calling it from a thread that didn't enter through the proxy (e.g. a new thread you spawned) throws the same exception because the ThreadLocal isn't set there. - It couples your business code to Spring's AOP internals — generally considered a code smell; prefer restructuring. ## When to use As a last-resort escape hatch when you truly must invoke another advised method on the same bean and can't refactor. Better alternatives: extract the advised method into a **separate bean**, use **self-injection** (inject the bean into itself), or switch to **AspectJ load-time/compile-time weaving**, which advises the actual bytecode so `this` calls are also intercepted.
- What happens if you call AopContext.currentProxy() when exposeProxy is false?It throws IllegalStateException with a message like 'Cannot find current proxy: Set exposeProxy property on Advised to true' — the ThreadLocal was never populated.
- Name two cleaner alternatives to using AopContext for self-invocation.Extract the advised method into a separate bean so the call crosses a proxy boundary, or use self-injection (inject the bean into itself and call through that reference). AspectJ weaving avoids the problem entirely.
saying these in an interview costs you the question
- Thinking @Transactional works on internal this.method() calls by default
- Believing exposeProxy is on by default
- Confusing AopContext.currentProxy() with getting the target object (it returns the proxy, not the raw target)