How does exposeProxy / AopContext.currentProxy() actually work, and when is it the right tool?
answer
- exposeProxy stores proxy in ThreadLocal
- AopContext.currentProxy() reads it
- cast + call annotated method through it
- missing flag -> IllegalStateException
- thread-bound; couples code to Spring AOP
basics
~10 sWith exposeProxy=true, the proxy stores itself in a thread-local before delegating to the target. Inside the method you call AopContext.currentProxy() to get that proxy and invoke the annotated method through it, so advice runs.
solid answer
~40 sNormally the target has no handle on its proxy. Setting exposeProxy=true (e.g. @EnableAspectJAutoProxy(exposeProxy=true), or on @EnableTransactionManagement/@EnableCaching) tells Spring's ExposeInvocationInterceptor path to publish the current proxy into an AopContext thread-local for the duration of each proxied call. Inside your method you retrieve it with AopContext.currentProxy(), cast to your type, and call the annotated method through it — now the call crosses the proxy boundary and advice fires. Trade-offs: it couples business code to org.springframework.aop.framework.AopContext, requires the global flag (else IllegalStateException: Cannot find current proxy), and only works while executing inside a proxied entry call. It's a localized escape hatch — I prefer refactoring to a separate bean, and reach for exposeProxy only when extraction is genuinely disruptive.
code
java · 22 lines@Configuration
@EnableAspectJAutoProxy(exposeProxy = true) // required
class AopConfig {}
@Service
class ReportService {
public void generateBatch(List<Long> ids) {
// Retrieve THIS bean's proxy from the AopContext thread-local
ReportService self = (ReportService) AopContext.currentProxy();
for (Long id : ids) {
self.generate(id); // goes through proxy -> @Cacheable honored
}
}
@Cacheable("reports")
public Report generate(Long id) {
return expensiveBuild(id);
}
}
// Without exposeProxy=true, AopContext.currentProxy() throws
// IllegalStateException: "Cannot find current proxy..."go deeper
Likely unfamiliar; enough to know a flag exists to reach the proxy from inside.
Knows you enable exposeProxy and call AopContext.currentProxy(), plus the missing-flag error.
Explains the thread-local mechanism, thread-boundedness, and the coupling trade-off vs alternatives.
Judges when leaking AopContext into business code is acceptable and contrasts it with self-injection and AspectJ at an architecture level.
## The problem it solves The target object doesn't know it's proxied and can't route its own calls back through the wrapper. `exposeProxy` gives the target a way to **retrieve its own proxy at runtime** so it can deliberately re-enter through it. ## How it works mechanically 1. You enable it globally: `@EnableAspectJAutoProxy(exposeProxy = true)` (and the transaction/cache auto-proxy configs honor an equivalent so their proxies expose too). This sets `exposeProxy=true` on the generated `Advised` proxies. 2. When an **external** call enters the proxy, before delegating, Spring's AOP framework calls `AopContext.setCurrentProxy(proxy)`, storing the proxy in a **thread-local** (`ThreadLocal<Object>`), and restores the previous value in a `finally` after the call. 3. While your target method runs (on that same thread, within that call), `AopContext.currentProxy()` returns that stored proxy. 4. You cast it to your bean type and invoke the annotated method: `((MyService) AopContext.currentProxy()).cachedMethod()`. Because you're now calling **on the proxy**, the interceptor chain runs. ```java @Configuration @EnableAspectJAutoProxy(exposeProxy = true) class AopConfig {} @Service class ReportService { public void generateAll(List<Id> ids) { ReportService proxy = (ReportService) AopContext.currentProxy(); for (Id id : ids) proxy.generate(id); // @Cacheable applies } @Cacheable("reports") public Report generate(Id id) { /* expensive */ } } ``` ## Requirements and failure modes - If the flag is **not** enabled, `AopContext.currentProxy()` throws `IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true'.` - It only works **while executing inside a proxied invocation** on the current thread. If the code runs on a **different thread** (e.g. handed to an executor without propagating the context), the thread-local is empty and it fails. - It relies on proxies, so it still can't advise `private`/`final` methods (CGLIB limitation). - There's a tiny per-call cost to set/clear the thread-local — negligible in practice. ## Trade-offs and when to use it Pros: no extra bean, no self-field, keeps everything in one class. Cons: it **leaks Spring's AOP API (`AopContext`) into business logic**, needs a global config flag that's easy to forget, and the explicit cast is unpleasant and fragile if the type changes. It's essentially a manual, imperative version of self-injection. **Use it** when refactoring into a separate collaborator is disruptive and you want a self-contained fix, or in framework-ish code that already depends on Spring internals. **Avoid it** as a default — prefer extracting a collaborator bean (clean, testable) or, for pervasive internal-advice needs, AspectJ weaving.
- What error do you get if exposeProxy isn't enabled?IllegalStateException with the message 'Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true'.'
- Does AopContext.currentProxy() work if the internal call runs on a different thread?No — the proxy is held in a thread-local bound to the thread of the proxied entry call. On a separate thread (e.g. an executor) the context is empty and it fails, unless you explicitly propagate it.
saying these in an interview costs you the question
- Thinking exposeProxy is enabled by default
- Believing it removes the proxy model (it doesn't — it exposes the proxy)
- Assuming the thread-local survives across threads automatically
- Using it as the go-to fix instead of refactoring