skip to content

Why do internal (self-invocation) method calls in a Spring bean skip @Transactional/@Cacheable advice, and what does setting exposeProxy=true do about it?

level: juniorimportance: must knowfreq 70%

answer

  1. this bypasses the proxy
  2. advice lives on proxy not target
  3. ThreadLocal holds current proxy
  4. cast AopContext.currentProxy()
  5. default off → IllegalStateException

basics

~20 s

Spring 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 s

Spring 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
java
@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

for a junior

Must know that self-calls skip advice and that this is why an internal @Transactional 'does nothing'.

for a middle

Should know exposeProxy=true + AopContext.currentProxy() as the fix and the cast requirement.

for a senior

Should weigh it against self-injection / separate bean and know the IllegalStateException failure mode.

for a principal

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)

context