skip to content

How do you enable exposeProxy and correctly retrieve/use AopContext.currentProxy() to re-route an internal call through the proxy?

level: middleimportance: should knowfreq 50%

answer

  1. @EnableAspectJAutoProxy(exposeProxy=true)
  2. cast to interface for JDK proxy
  3. cast to class for CGLIB
  4. ThreadLocal set on proxy entry
  5. self-injection = cleaner equivalent

basics

~20 s

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

First 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 lines
java
public 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

for a junior

Knows the flag + cast pattern at a recipe level.

for a middle

Understands JDK-vs-CGLIB cast rules and that it must run on the proxied thread.

for a senior

Knows the ThreadLocal set/restore mechanics and why async/new-thread lookups fail.

for a principal

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

context