How do you enable exposeProxy and correctly retrieve/use AopContext.currentProxy() to re-route an internal call through the proxy?
answer
- @EnableAspectJAutoProxy(exposeProxy=true)
- cast to interface for JDK proxy
- cast to class for CGLIB
- ThreadLocal set on proxy entry
- self-injection = cleaner equivalent
basics
~20 sTurn on the flag with @EnableAspectJAutoProxy(exposeProxy = true). Inside the bean, cast the exposed proxy to your type and call the target method through it: ((MyService) AopContext.currentProxy()).method(). That routes the call through the proxy so advice runs.
solid answer
~40 sFirst enable proxy exposure — typically `@EnableAspectJAutoProxy(exposeProxy = true)`, which sets exposeProxy on the auto-proxy creator so every proxied invocation stores the proxy in a ThreadLocal. Then, inside a method that is itself running through the proxy, retrieve it with `AopContext.currentProxy()`, cast it to the bean's interface or class, and invoke the other advised method on that reference. The cast type must match how the proxy was created: an interface for a JDK dynamic proxy, or the concrete class for a CGLIB proxy. Because the value comes from a ThreadLocal set at proxy entry, it's only valid on the same thread within the current invocation — if you hand work to another thread, the lookup fails. Keep the cast and call minimal and consider self-injection as a less intrusive alternative.
code
java · 20 linespublic interface ReportService { void generate(); void render(); }
@Service
public class ReportServiceImpl implements ReportService {
@Override
public void generate() {
// JDK proxy => MUST cast to the INTERFACE, not ReportServiceImpl
((ReportService) AopContext.currentProxy()).render();
}
@Override
@Cacheable("reports")
public void render() { /* heavy work, now actually cached */ }
}
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableCaching
class Config {}go deeper
Knows the flag + cast pattern at a recipe level.
Understands JDK-vs-CGLIB cast rules and that it must run on the proxied thread.
Knows the ThreadLocal set/restore mechanics and why async/new-thread lookups fail.
Chooses self-injection or redesign over touching AopContext and reasons about proxy-type implications on public API.
## Two steps: enable, then retrieve ### 1. Enable exposure The proxy is only published when `exposeProxy` is true. Common ways to set it: - **`@EnableAspectJAutoProxy(exposeProxy = true)`** — the usual switch. It configures the `AnnotationAwareAspectJAutoProxyCreator` so all auto-created proxies expose themselves. - **Programmatic** — on a `ProxyFactory` / `ProxyFactoryBean`, call `setExposeProxy(true)`. Anything extending `org.springframework.aop.framework.ProxyConfig` has this property. When set, Spring's `JdkDynamicAopProxy` / `CglibAopProxy` do roughly: `Object oldProxy = AopContext.setCurrentProxy(proxy);` at the start of an invocation and restore it in a `finally`. `AopContext` stores it in a `private static final ThreadLocal<Object> currentProxy`. ### 2. Retrieve and call ```java MyService self = (MyService) AopContext.currentProxy(); self.otherAdvisedMethod(); ``` The returned object **is the proxy**, not the target. Calling a method on it re-enters the proxy, so advice (transactions, caching, custom aspects) executes. ## The cast must match the proxy kind - **JDK dynamic proxy** (bean implements interfaces) → the proxy implements those **interfaces** only, not your concrete class. You must cast to an **interface** (`(MyServiceApi) AopContext.currentProxy()`). Casting to the concrete class throws `ClassCastException`. - **CGLIB proxy** (no interface, or `proxyTargetClass=true`) → the proxy is a **subclass** of your class, so casting to the concrete class works. ## ThreadLocal scoping — the big edge case The proxy is stored per-thread and only for the duration of the current proxied call. Therefore: - It works only **inside** a call that entered through the proxy. - If you call it from a **freshly spawned thread**, an `@Async` handoff target, or a reactive scheduler thread that didn't enter via the proxy, you get `IllegalStateException`. - After the outer proxied call returns, the ThreadLocal is cleared (restored in `finally`). ## Failure modes - `exposeProxy=false` → `IllegalStateException: Cannot find current proxy…`. - Wrong cast type → `ClassCastException`. - Calling from the wrong thread → `IllegalStateException`. ## When to use vs alternatives Use it only when refactoring isn't feasible. Cleaner options: extract the second method into its own bean, or **self-injection**: ```java @Autowired private MyService self; // injected proxy // self.otherAdvisedMethod(); ``` Self-injection is the same idea without touching AOP internals, and Spring handles the circular reference for a singleton.
- You cast AopContext.currentProxy() to the concrete class and get a ClassCastException. Why?The bean implements an interface, so Spring made a JDK dynamic proxy that implements only the interface, not your concrete class. Cast to the interface — or force CGLIB with proxyTargetClass=true if you need the class type.
- Why does AopContext.currentProxy() fail inside a @Async method the bean started?@Async runs on a different thread. The proxy is stored in a ThreadLocal set when the outer call entered the proxy; the async thread never entered through it, so the ThreadLocal is empty and it throws IllegalStateException.
saying these in an interview costs you the question
- Assuming the cast can always be to the concrete class
- Thinking currentProxy() returns the target object
- Expecting it to work across threads / in @Async bodies