skip to content

Walk through what happens internally when a method is called on a Spring AOP proxy, from ProxyFactory config down to the advisor chain and TargetSource.

level: principalimportance: nice to knowfreq 20%

answer

  1. DefaultAopProxyFactory picks JDK vs CGLIB
  2. getTarget -> build chain -> ReflectiveMethodInvocation.proceed()
  3. getInterceptorsAndDynamicInterceptionAdvice, methodCache
  4. proceed() cursor recurses, ends in invokeJoinpoint reflection
  5. exposeProxy -> AopContext; releaseTarget in finally

basics

~20 s

A call hits the proxy's invoke handler. It asks the TargetSource for the target, builds the list of matching interceptors for that method from the advisors, then runs them one by one via a MethodInvocation whose proceed() steps through the chain and finally calls the target method by reflection.

solid answer

~40 s

The `ProxyFactory` (an `AdvisedSupport`) is handed to an `AopProxy` — `JdkDynamicAopProxy` if the target has interfaces, else `CglibAopProxy`. On each call, its handler (`invoke`/`intercept`) does roughly: obtain the target from `advised.getTargetSource().getTarget()`; compute the method's interceptor chain via `advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)`, which walks the `Advisor[]`, evaluates each `Pointcut` against the method, and returns the matching `MethodInterceptor`s (cached in `AdvisedSupport.methodCache`). It then creates a `ReflectiveMethodInvocation` (JDK) or `CglibMethodInvocation`, and calls `proceed()`. `proceed()` recursively advances an index through the interceptor list, invoking each; when the list is exhausted it does `invokeJoinpoint` — a reflective call on the target. Finally `releaseTarget()` runs. Config like `exposeProxy` binds the proxy to `AopContext` around the call.

code

java · 21 lines
java
// Conceptual sketch of the JDK handler's core (simplified)
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    TargetSource ts = this.advised.getTargetSource();
    Object target = ts.getTarget();               // resolve per call
    Class<?> targetClass = (target != null ? target.getClass() : null);
    try {
        if (this.advised.isExposeProxy()) AopContext.setCurrentProxy(proxy);
        // build (and cache) the matching interceptor chain for this method
        List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(
                method, targetClass);
        if (chain.isEmpty()) {
            return AopUtils.invokeJoinpointUsingReflection(target, method, args);
        }
        MethodInvocation mi = new ReflectiveMethodInvocation(
                proxy, target, method, args, targetClass, chain);
        return mi.proceed();                       // walk interceptors -> target
    } finally {
        if (target != null && !ts.isStatic()) ts.releaseTarget(target);
        // restore previous AopContext if exposeProxy was set
    }
}

go deeper

for a junior

Know that a call goes through advice, then to the target.

for a middle

Describe getTarget(), the interceptor chain, and proceed() reaching the target by reflection.

for a senior

Explain chain building/caching, JDK vs CGLIB handlers, exposeProxy, and releaseTarget timing.

for a principal

Reason about how the Advised/AdvisorChainFactory/TargetSource/MethodInvocation separation enables runtime reconfiguration, pooling, and the performance implications of dynamic matchers and self-invocation.

**Setup phase (ProxyFactory / AdvisedSupport).** When you configure a `ProxyFactory`, you populate an `AdvisedSupport`: the `TargetSource`, the `Advisor[]` chain, proxied interfaces, and flags (`proxyTargetClass`, `exposeProxy`, `frozen`, `optimize`). `getProxy()` calls `createAopProxy()` on `ProxyCreatorSupport`, which delegates to the configured `AopProxyFactory` (default `DefaultAopProxyFactory`). That factory picks: - `JdkDynamicAopProxy` — when `proxyTargetClass` is false and the target exposes interfaces. Implements `InvocationHandler`. - `CglibAopProxy` (`ObjenesisCglibAopProxy`) — when there are no interfaces, `proxyTargetClass=true`, or the target is a class. Generates a subclass whose `MethodInterceptor` (`DynamicAdvisedInterceptor`) routes calls. **Invocation phase — step by step (JDK path in `JdkDynamicAopProxy.invoke`):** 1. **Special methods.** `equals`/`hashCode`/`DecoratingProxy`/`Advised` methods may be handled directly without hitting the target. 2. **exposeProxy.** If `advised.isExposeProxy()`, push the current proxy onto a `ThreadLocal` via `AopContext.setCurrentProxy(proxy)` so target code can call `AopContext.currentProxy()` to route self-invocations through the proxy. 3. **Resolve target.** `TargetSource targetSource = advised.getTargetSource(); target = targetSource.getTarget();` — for a `SingletonTargetSource` this is the cached instance; for prototype/pooled/thread-local it may create/borrow one. `targetClass` is derived for pointcut matching. 4. **Build the interceptor chain.** `List<Object> chain = advised.getAdvisorChainFactory().getInterceptorsAndDynamicInterceptionAdvice(advised, method, targetClass)`. This iterates the `Advisor[]`: for each, its `Pointcut` (class filter + method matcher) is checked against the method; matches contribute `MethodInterceptor`s (advices are adapted via `AdvisorAdapterRegistry` — e.g. a `MethodBeforeAdvice` becomes a `MethodBeforeAdviceInterceptor`). Runtime (`isRuntime()`) matchers become `InterceptorAndDynamicMethodMatcher` entries evaluated per-args at call time. The result is **cached** in `AdvisedSupport.methodCache` keyed by method; mutating advisors evicts it. 5. **Empty chain fast-path.** If the chain is empty, the framework may invoke the target directly via `AopUtils.invokeJoinpointUsingReflection` (an optimization, avoiding a `MethodInvocation`). 6. **Create the invocation.** Otherwise construct `ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain)`. 7. **proceed().** `invocation.proceed()` holds a cursor `currentInterceptorIndex`. Each call increments it and invokes `chain.get(i).invoke(this)`. An around `MethodInterceptor` receives the `MethodInvocation` and typically calls `mi.proceed()` again to continue. When the cursor reaches the end, `proceed()` calls `invokeJoinpoint()` → reflective `method.invoke(target, args)` (CGLIB uses a `MethodProxy` fast invoke). 8. **Return / finally.** The return value bubbles back out through each interceptor (letting after/around code run). In a `finally`: if a target was obtained from a non-static source, `targetSource.releaseTarget(target)`; if `exposeProxy` was set, restore the previous `AopContext` value. **CGLIB path.** Nearly identical, but the subclass's `DynamicAdvisedInterceptor.intercept` does the same resolve-chain-then-proceed dance, using `CglibMethodInvocation` (a `ReflectiveMethodInvocation` subclass) and `MethodProxy.invoke` for a faster-than-reflection target call. **Why the design matters.** The split — `Advised` config, `AdvisorChainFactory` for chain building/caching, `TargetSource` for target lifecycle, `MethodInvocation.proceed()` for the recursive chain — is what lets Spring vary each axis independently: advices can be added/removed at runtime, targets pooled/swapped, and the same engine serves both hand-built `ProxyFactory` proxies and container auto-proxies. **Gotchas / edge cases.** (1) **Self-invocation**: because the target's `this` is the raw object, internal calls skip the chain unless `exposeProxy` + `AopContext.currentProxy()` are used. (2) **Chain cache**: dynamic (runtime) method matchers can't be fully cached and cost per-call evaluation. (3) **releaseTarget** must be paired with `getTarget()` — custom `TargetSource`s that borrow from a pool must release in the finally, which the framework guarantees. (4) **Frozen + empty chain + static target** enables the fastest path and CGLIB `optimize`. (5) `equals`/`hashCode` on proxies are special-cased; two proxies aren't necessarily equal to their target. (6) `final` methods can't be advised under CGLIB (not overridable) and simply call through.

  • Where is the per-method interceptor chain cached, and what invalidates it?
    In AdvisedSupport.methodCache, keyed by method (with target class). Adding/removing an advisor or changing the target source invalidates it, so the next call recomputes which advisors match. Dynamic runtime method matchers still evaluate per invocation and aren't fully cached.
  • Why does self-invocation on the target skip the advice, and how does exposeProxy help?
    The target method calls this.other(), and `this` is the raw target object, not the proxy, so the call never enters the invocation handler. Setting exposeProxy=true publishes the proxy to a ThreadLocal via AopContext; the target can call AopContext.currentProxy() and invoke through the proxy to re-enter the chain.

saying these in an interview costs you the question

  • Claiming the proxy calls the target directly without a MethodInvocation chain
  • Thinking the target is stored on the proxy rather than fetched from TargetSource per call
  • Assuming self-invocations are advised by default

context