What this post covers
This is the tenth post in the "Digging into Spring's Source" series. Up to Part 9 we looked at how the container reads blueprints and builds beans (Phase 1). This post and the next are Phase 2, AOP. In Part 4 I only said "a bean can be replaced by a proxy in the after-initialisation hook of BeanPostProcessor" and moved on. This time we see how that proxy is actually built.
This post deliberately leaves the container out and reads only the aop.framework package of the spring-aop module. Auto-proxying (the container wrapping beans by itself) and @Aspect are the subject of Part 11.
There are four questions to answer.
- On what basis does
ProxyFactorychoose between a JDK dynamic proxy and a CGLIB proxy? - How does each of the two build the proxy object?
- When a method is called on the proxy, where does the call go?
- Why is a proxy not applied when an object calls its own method from the inside (self-invocation)?
| Item | Details |
|---|---|
| Source | Spring Framework 7.1.0-SNAPSHOT |
| Commit | f447f3c310 (2026-10-01, main branch) |
| Module read most | spring-aop (plus org.springframework.cglib in spring-core) |
| Classes read most | ProxyFactory, AdvisedSupport, DefaultAopProxyFactory, JdkDynamicAopProxy, CglibAopProxy, ReflectiveMethodInvocation |
Every line number and every detail in this post refers to that commit. In other versions the line numbers will differ and some details may too. Part 1 explains how to get the source at the same commit.
My study notes go much deeper, line by line. This post keeps only the main thread that someone seeing Spring for the first time can follow. What was left out is named at the end, under "Left out of this post".
Words to know first
A proxy is an object that wraps the original and stands in for it. From the outside it looks like the same type, but when a method is called it does something extra before and after and then passes the call on to the original. In this post the wrapped original is called the target. The same word is explained in the "Words to know first" of Part 4.
AOP (aspect-oriented programming) is a way of keeping code that cuts across many classes in the same way (logging, transactions) in one place and slipping it in without editing each class. Spring's AOP does that slipping in by building a proxy and handing it out instead of the target. So understanding Spring AOP starts with understanding proxies.
Advice is "the behaviour to slip in". The only kind of advice this post uses is the most basic one, a MethodInterceptor. It has a single method, invoke(MethodInvocation), and calling invocation.proceed() inside it moves on to the next step (another interceptor, or the target method) (MethodInterceptor lines 44–59, Joinpoint line 51). The package is org.aopalliance because these are shared interfaces meant for several AOP implementations, and Spring ships them as they are.
A pointcut is the condition that says "which methods of which classes to slip into", and an advisor is an advice and a pointcut bundled as a pair. A Pointcut consists of a ClassFilter that filters classes and a MethodMatcher that filters methods; Pointcut.TRUE means "every method" (Pointcut lines 33–53). PointcutAdvisor is getAdvice() plus getPointcut() (Advisor line 55, PointcutAdvisor lines 26–33).
A TargetSource is "the place the target comes from". The proxy does not hold the target directly; it asks the TargetSource for it with getTarget() (TargetSource lines 36–78). Usually it is a SingletonTargetSource, which simply returns the one object it holds, and whose isStatic() is true, meaning "the same object every time" (SingletonTargetSource lines 66–74). There are implementations that create a new object each time or lend one from a pool, but this post does not cover them.
A JDK dynamic proxy is a class that java.lang.reflect.Proxy from the Java standard library generates at run time. It builds a class that implements the given interfaces, and every method call on it is routed to a single InvocationHandler.invoke(). It needs interfaces to work.
CGLIB is a library that generates bytecode at run time to stamp out a subclass of a class. It can wrap classes that implement no interface at all. Spring does not depend on CGLIB as a separate library; it carries a repackaged copy inside spring-core as the org.springframework.cglib package. The package description says it is a repackaging of CGLIB 3.3 with Spring-specific patches, for internal use only (spring-core/.../cglib/package-info.java lines 1–11). When this post says CGLIB's Enhancer, it means spring-core/src/main/java/org/springframework/cglib/proxy/Enhancer.java.
1. ProxyFactory — the object that collects the settings
ProxyFactory is the entry point for building a proxy by hand. Even when the container wraps beans on its own, it ends up creating a ProxyFactory inside (AbstractAutoProxyCreator line 447, covered in Part 11). So knowing this one class makes both paths readable.
ProxyFactory is a four-level inheritance chain, and each level has its own job.
The top three levels are "settings"; the bottom one is "building". The blue box is the setting flags; the yellow box is the cache of section 4.
| Class | Responsibility | Where |
|---|---|---|
ProxyConfig |
Five flags: proxyTargetClass, optimize, opaque, exposeProxy, frozen |
lines 33–47 |
AdvisedSupport |
The TargetSource, the list of interfaces to proxy, the list of advisors, the per-method interceptor chain cache |
lines 68–129 |
ProxyCreatorSupport |
createAopProxy() — hands the settings to an AopProxyFactory and gets back an AopProxy |
lines 33–106 |
ProxyFactory |
getProxy() — asks that AopProxy to build the proxy object |
lines 36–98 |
All five flags are Boolean and read as false unless set (ProxyConfig lines 69–71). Three of them matter for this post.
| Flag | Meaning | Javadoc |
|---|---|---|
proxyTargetClass |
Proxy the target class itself rather than its interfaces (= force CGLIB) | lines 50–61 |
optimize |
"Aggressive optimizations". With the current implementation it forces CGLIB but skips the class validation (final-method checks and so on) | lines 73–81 |
exposeProxy |
Store the proxy being invoked in a ThreadLocal so that AopContext can hand it out. The star of section 6 |
lines 111–119 |
The methods that put things into AdvisedSupport do not store what they receive as it is; they wrap it once.
setTarget(obj)wraps the object in aSingletonTargetSourceand stores that astargetSource(lines 153–155).addAdvice(advice)wraps the advice in aDefaultPointcutAdvisorand adds that to the advisor list (lines 426–449). That advisor's pointcut isPointcut.TRUE, every method (DefaultPointcutAdvisorlines 41, 57–59). The list only ever contains advisors.- The
ProxyFactory(Object target)constructor callssetTargetand then registers every interface the target implements as an interface to proxy (lines 49–52). This matters for the selection rule in section 2.
And getProxy() is two lines.
public Object getProxy() {
return createAopProxy().getProxy();
}
spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java, lines 96–98
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
return getAopProxyFactory().createAopProxy(this);
}
spring-aop/src/main/java/org/springframework/aop/framework/ProxyCreatorSupport.java, lines 101–106
The this in createAopProxy(this) is the AdvisedSupport holding all the settings. The AopProxyFactory that receives it and returns an AopProxy is a single-method interface (lines 44–55), and its default implementation is DefaultAopProxyFactory.INSTANCE (ProxyCreatorSupport lines 46–48). The AopProxy that comes back is also a small interface with only getProxy() and getProxyClass() (lines 32–64). It has two implementations: JdkDynamicAopProxy and CglibAopProxy.
Easy to get wrong
ProxyFactoryis not a proxy; it is configuration. The Javadoc ofAdvisedSupportsays so: "These are not themselves AOP proxies" (lines 52–53). A new proxy is created every timegetProxy()is called, and interfaces or advice changed in between show up in the next proxy (ProxyFactorylines 88–95). The example in section 6 relies on this.- The finished proxy is also an
Advised. TheAdvisedinterface thatAdvisedSupportimplements (line 38) is also implemented by the proxy object (section 3). So you can cast a proxy toAdvisedand inspect its advisors or itsTargetSource. Turn on theopaqueflag to prevent that.
2. The rule for choosing one of the two — DefaultAopProxyFactory
The whole decision is in one method. This is the source as it is.
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || !config.hasUserSuppliedInterfaces()) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null && config.getProxiedInterfaces().length == 0) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass == null || targetClass.isInterface() ||
Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java, lines 59–76
The green boxes are the outcomes. Even on the CGLIB road there are three exits back to JDK.
Read top to bottom:
- The default is a JDK dynamic proxy. If all three conditions of the first
ifare false, it is aJdkDynamicAopProxyright away. The three conditions are:optimizeis on,proxyTargetClassis on, or there is not a single user-supplied interface. - If any one of the three is true, we head towards CGLIB. But if the target class is
null, is itself an interface, is already a JDK proxy class, or is a lambda class, CGLIB cannot subclass it, so it is aJdkDynamicAopProxyafter all. Lambda classes are detected byClassUtils.isLambdaClass(lines 946–949). - What remains, an ordinary class, becomes an
ObjenesisCglibAopProxy. The Objenesis in the name is explained in section 5.
"User-supplied interfaces" is decided by hasUserSuppliedInterfaces(). It walks the interface list, ignores the SpringProxy family and interfaces introduced by advisors, and returns true if anything is left (AdvisedSupport lines 268–275).
Examples ①②③: one target, three outcomes
These examples were compiled and run with the release version, Spring Framework 7.0.9, and produced the output shown below (I did not build and run the 7.1.0-SNAPSHOT this series reads). The only interceptor prints one line before and one after each call.
// An example written for this post (not Spring's code)
interface Greeter {
String greet(String name);
}
static class GreeterImpl implements Greeter {
public String greet(String name) { return "Hello, " + name; }
}
/** An interceptor that prints a line before and after the call. */
static class LogInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("-> before " + invocation.getMethod().getName());
Object result = invocation.proceed(); // on to the next interceptor, or the target method
System.out.println("<- after " + invocation.getMethod().getName());
return result;
}
}
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new GreeterImpl()); // proxies every interface the target implements
factory.addAdvice(new LogInterceptor());
Object proxy = factory.getProxy();
System.out.println("class = " + proxy.getClass().getName());
System.out.println("isProxyClass = " + Proxy.isProxyClass(proxy.getClass()));
System.out.println("isJdkDynamicProxy = " + AopUtils.isJdkDynamicProxy(proxy));
System.out.println("interfaces = " + Arrays.toString(proxy.getClass().getInterfaces()));
// ... also prints instanceof checks and ((Advised) proxy).getAdvisors().length
Greeter greeter = (Greeter) proxy; // the interface type works
System.out.println(greeter.greet("Spring"));
try {
GreeterImpl impl = (GreeterImpl) proxy; // the implementation class does not
System.out.println(impl.greet("never"));
}
catch (ClassCastException ex) {
System.out.println("ClassCastException: " + ex.getMessage());
}
}
① Hand over a target that implements an interface, and you get a JDK dynamic proxy.
class = ex.u09.$Proxy0
isProxyClass = true
isJdkDynamicProxy = true
interfaces = [interface ex.u09.JdkProxyExample$Greeter, interface org.springframework.aop.SpringProxy, interface org.springframework.aop.framework.Advised, interface org.springframework.core.DecoratingProxy]
instanceof Greeter = true
instanceof GreeterImpl = false
advisors = 1
-> before greet
<- after greet
Hello, Spring
ClassCastException: class ex.u09.$Proxy0 cannot be cast to class ex.u09.JdkProxyExample$GreeterImpl (ex.u09.$Proxy0 and ex.u09.JdkProxyExample$GreeterImpl are in unnamed module of loader 'app')
The class name $Proxy0 means a class the JDK generated at run time. The proxy implements Greeter but does not extend GreeterImpl, so casting it to the implementation class throws ClassCastException. The interface list shows SpringProxy, Advised and DecoratingProxy, which I never asked for; section 3 explains them.
② Call setProxyTargetClass(true) on the same target and you get CGLIB. ③ A class with no interface at all (Counter) gets CGLIB without any setting. Here is the output of both.
[2] class = ex.u09.CglibProxyExample$GreeterImpl$$SpringCGLIB$$0
[2] superclass = ex.u09.CglibProxyExample$GreeterImpl
[2] isCglibProxy = true
[2] interfaces = [interface ex.u09.CglibProxyExample$Greeter, interface org.springframework.aop.SpringProxy, interface org.springframework.aop.framework.Advised, interface org.springframework.cglib.proxy.Factory]
[2] instanceof GreeterImpl = true
-> greet
Hello, CGLIB
[3] proxyTargetClass = false
[3] class = ex.u09.CglibProxyExample$Counter$$SpringCGLIB$$0
[3] isCglibProxy = true
-> next
-> next
[3] next() = 1, 2
This time the proxy class is a subclass of GreeterImpl, so the cast to the implementation class works. The $$SpringCGLIB$$ in the class name is the tag added by SpringNamingPolicy, the naming rule Spring plugs into CGLIB (SpringNamingPolicy lines 35, 44–74). In ③ proxyTargetClass is false and it is still CGLIB: that is the third condition of the rule above, "no user-supplied interfaces".
The three AopUtils methods that tell proxies apart all check "is an instance of SpringProxy" first, then Proxy.isProxyClass on the JDK side and "class name contains $$" on the CGLIB side (AopUtils lines 82–112, ClassUtils.CGLIB_CLASS_SEPARATOR line 92).
Easy to get wrong: "interfaces mean JDK" is only half true
- What the rule looks at is not whether the target class implements an interface, but what is in the interface list of
AdvisedSupport. The two look the same only because thenew ProxyFactory(target)constructor fills the list from the target automatically. Use the empty constructor and onlysetTarget(), and the list is empty, so even a class with interfaces gets CGLIB. - Even with
proxyTargetClasson, a target class that is itself an interface, or a lambda, gets a JDK proxy. That is the secondifin the code above. - Either way the proxy implements every interface the target implements. In the output of ②, the CGLIB proxy implements
Greetertoo.
3. The JDK way — a fake object that implements the interfaces
JdkDynamicAopProxy is an AopProxy and, at the same time, an InvocationHandler (line 69). One object both builds the proxy and handles the calls the proxy receives.
The building side is one line.
@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
return Proxy.newProxyInstance(determineClassLoader(classLoader), this.cache.proxiedInterfaces, this);
}
spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java, lines 116–122
Proxy.newProxyInstance(classLoader, interfaces, handler) is standard Java. The third argument is this, so whichever method of the proxy is called, it arrives at this object's invoke().
The second argument, proxiedInterfaces, is computed in the constructor with AopProxyUtils.completeProxiedInterfaces(config, true) (lines 94–108, line 324). That method appends three interfaces after the user-supplied ones (AopProxyUtils lines 190–222). That is why three unfamiliar interfaces showed up in the output of ①.
| Interface added automatically | Role |
|---|---|
SpringProxy |
A marker interface with no methods (lines 27–29): the stamp that says "this is a proxy made by Spring". AopUtils.isAopProxy checks for it |
Advised |
Lets the proxy's configuration (advisors, TargetSource, ...) be inspected and changed from outside. Left out when opaque |
DecoratingProxy |
A single method, getDecoratedClass(), asking for the real class behind the proxy (spring-core lines 37–47). Added to JDK proxies only |
Advised is left out when opaque is on. The reason DecoratingProxy is added only on the JDK side is in that interface's Javadoc: a CGLIB proxy is a subclass of the target class, so the target class can be read off the proxy class itself and the interface is not needed (lines 23–27). CglibAopProxy calls the one-argument overload, that is decoratingProxy = false (AopProxyUtils lines 173–175, CglibAopProxy line 202). That is why the output of ② has no DecoratingProxy and instead has Factory, which CGLIB itself adds.
The receiving side: invoke()
When a method of the proxy is called, invoke(proxy, method, args) runs. It is long, but the main line fits in one picture.
The blue box is the exposeProxy setting (section 6); the yellow box is the per-method chain cache (section 4).
@Override
public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// ...
try {
if (!this.cache.equalsDefined && AopUtils.isEqualsMethod(method)) {
// The target does not implement the equals(Object) method itself.
return equals(args[0]);
}
// ... hashCode, DecoratingProxy and Advised methods are handled first in the same way
else if (!this.advised.isOpaque() && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
// Service invocations on ProxyConfig with the proxy config...
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
}
Object retVal;
if (this.advised.isExposeProxy()) {
// Make invocation available if necessary.
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
// Get as late as possible to minimize the time we "own" the target,
// in case it comes from a pool.
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);
// Get the interception chain for this method.
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
// Check whether we have any advice. If we don't, we can fall back on direct
// reflective invocation of the target, and avoid creating a MethodInvocation.
if (chain.isEmpty()) {
// ...
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// We need to create a method invocation...
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
retVal = invocation.proceed();
}
// Massage return value if necessary.
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target &&
returnType != Object.class && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
// ...
retVal = proxy;
}
// ...
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
// Must have come from TargetSource.
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java, lines 162–251
Five pieces, top to bottom.
- Special methods are filtered out first. If
equals/hashCodeare not declared on the interfaces, they are not sent to the target; the proxies are compared with each other. Methods ofDecoratingProxyandAdvisedare answered by the proxy configuration object (this.advised), not the target. That is why((Advised) proxy).getAdvisors()worked in ①. - If
exposeProxy, the proxy is stored inAopContext. We come back to this in section 6. - The target is fetched and this method's interceptor chain is looked up. The subject of section 4.
- If the chain is empty, no
MethodInvocationis created and the target method is called directly by reflection, so that a method with no advice costs nothing extra. If it is not empty, aReflectiveMethodInvocationis created andproceed()is called. - If the return value is the target itself (
this), it is swapped for the proxy. If the target didreturn this;and that were passed through, the caller would end up holding the bare target instead of the proxy, and advice would be missing from every call after that.
Notice also that finally calls releaseTarget only when the TargetSource is not isStatic(). A target borrowed from a pool is returned; with a SingletonTargetSource, always the same object, nothing happens.
Easy to get wrong
- The result of
equalsis not "wraps the same target". Comparing proxies checks whether the interfaces, the advisors and theTargetSourceare equal (lines 254–286). Comparing a proxy with the bare target byequalsgivesfalse. - The
return thisfix has conditions. It is skipped when the return type isObject, and the return type must be a type the proxy implements (lines 224–226 of the code above). The comment that nothing can be done if the target hides a reference to itself inside another returned object is still there.
4. Where the call goes — the interceptor chain
The getInterceptorsAndDynamicInterceptionAdvice(method, targetClass) of section 3 returns "the list of interceptors to apply to this method". Filtering the advisor list again for every call would be slow, so the result is cached per method.
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class<?> targetClass) {
List<Object> cachedInterceptors;
if (this.methodCache != null) {
// Method-specific cache for method-specific pointcuts
MethodCacheKey cacheKey = new MethodCacheKey(method);
cachedInterceptors = this.methodCache.get(cacheKey);
if (cachedInterceptors == null) {
cachedInterceptors = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
this, method, targetClass);
this.methodCache.put(cacheKey, cachedInterceptors);
}
}
else {
// Shared cache since there are no method-specific advisors (see below).
cachedInterceptors = this.cachedInterceptors;
// ...
}
return cachedInterceptors;
}
spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java, lines 516–538
There are two caches, methodCache (per method) and cachedInterceptors (shared), and the reason is in adviceChanged(). The per-method map is created only if at least one PointcutAdvisor is in the advisor list; without pointcuts every method has the same chain, so one is shared (lines 543–556). adviceChanged() is called whenever advisors or interfaces change, and it empties the caches (line 238, line 413).
The actual filtering is done by DefaultAdvisorChainFactory (lines 57–112). It walks the advisors in registration order; for a PointcutAdvisor it matches the class with the pointcut's ClassFilter and the method with its MethodMatcher, and on a match it converts the advice to a MethodInterceptor and adds it to the list. Our LogInterceptor already is a MethodInterceptor, so it goes in as it is (DefaultAdvisorAdapterRegistry lines 81–96). AopUtils.canApply, which asks up front "could this advisor apply to this class at all" (lines 239–274, lines 298–309), is used on the auto-proxy side to decide whether to wrap at all, so it belongs to Part 11.
What actually walks the chain is ReflectiveMethodInvocation.proceed().
@Override
public @Nullable Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher dm) {
// ...
}
else {
// It's an interceptor, so we just invoke it: The pointcut will have
// been evaluated statically before this object was constructed.
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
An interceptor does not know what comes after it. proceed() just bumps the index and calls the next one.
The index starts at -1, goes up by one on each proceed(), and when it reaches the end of the list invokeJoinpoint() calls the target method by reflection (lines 189–191, AopUtils.invokeJoinpointUsingReflection lines 351–373). When an interceptor calls invocation.proceed() inside its invoke(invocation), the index of the same invocation object goes up and the next interceptor runs. If an interceptor never calls proceed(), the chain stops there and the target method is not executed. In the output of ①, -> before greet is printed, then Hello, Spring is computed, then <- after greet is printed: that is this order.
Easy to get wrong
- The order of the interceptor list is the order of execution. The chain is built in the order the advisors were registered, and
invoke()is called in that order. The rules for ordering several advisors (@Orderand friends) are in Part 11. proceed()is meant to be called once. Call it twice on the sameinvocationand the index is already at the end, so the target method runs again. To call it again, clone withinvocableClone()(lines 203–210).
5. The CGLIB way — a subclass of the target class
CglibAopProxy builds a proxy that extends the class instead of implementing interfaces. Here is the middle of the code that builds it.
// Configure CGLIB Enhancer...
Enhancer enhancer = createEnhancer();
// ...
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
// ...
Callback[] callbacks = getCallbacks(rootClass);
// ...
ProxyCallbackFilter filter = new ProxyCallbackFilter(
this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset);
enhancer.setCallbackFilter(filter);
// ...
spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java, lines 192–219
Enhancer is CGLIB's class generator. Give it a superclass (setSuperclass) and the interfaces to implement (setInterfaces), and it produces the bytecode of a subclass that overrides every method that can be overridden. In the JDK way every call arrived at one invoke(); CGLIB instead takes an array of callbacks, and a CallbackFilter decides for each method "which callback number to use". That decision is made once per method when the class is generated, and baked into the class.
The blue box is the path that runs advice. The others are shortcuts straight to the target or the configuration, with no advice.
The first seven slots of the callback array are fixed.
Callback[] mainCallbacks = new Callback[] {
aopInterceptor, // for normal advice
targetInterceptor, // invoke target without considering advice, if optimized
new SerializableNoOp(), // no override for methods mapped to this
targetDispatcher, this.advisedDispatcher,
new EqualsInterceptor(this.advised),
new HashCodeInterceptor(this.advised)
};
spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java, lines 351–358
The slot numbers have names as constants at the top of the file (lines 89–95). Which method uses which number is decided by ProxyCallbackFilter.accept(method) (lines 826–909). An ordinary method with advice goes to slot 0, DynamicAdvisedInterceptor. Its intercept() has almost the same shape as the invoke() of section 3: the exposeProxy handling, getTarget(), the chain lookup, a direct call if the chain is empty, otherwise a ReflectiveMethodInvocation and proceed(), and the return-value fix (lines 710–751; the return-value fix is at lines 444–463). In other words, only the way the proxy is built differs; the way a call is sent down the chain is the same for both. Everything in section 4 applies to CGLIB as it is.
Built without calling the constructor — Objenesis
What the rule in section 2 returned was not CglibAopProxy but its subclass ObjenesisCglibAopProxy. The one difference is how the proxy instance is created. The parent uses enhancer.create() (lines 248–254); the child generates only the class, creates the instance with objenesis.newInstance(proxyClass, ...), and then plugs in the callback array.
Class<?> proxyClass = enhancer.createClass();
Object proxyInstance = null;
if (objenesis.isWorthTrying()) {
try {
proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
}
// ...
}
if (proxyInstance == null) {
// Regular instantiation via default constructor...
// ...
}
((Factory) proxyInstance).setCallbacks(callbacks);
return proxyInstance;
spring-aop/src/main/java/org/springframework/aop/framework/ObjenesisCglibAopProxy.java, lines 62–92
Objenesis is a library that creates objects without calling a constructor, and it too is repackaged into spring-core as org.springframework.objenesis (SpringObjenesis line 35). The class Javadoc says it creates proxy instances without invoking the constructor of the class and is used by default (lines 30–37). The proxy is a subclass of the target class, so creating it the normal way would run the target class's constructor a second time. The target object has already been built through its constructor; this avoids the constructor running twice just for the proxy. As a side effect, classes without a default constructor can be wrapped too. If it fails, it falls back to the default constructor.
Example ④: final methods cannot be intercepted
Here the limit of the subclass approach shows. In Java a final method cannot be overridden, so CGLIB cannot attach a callback to it either. CglibAopProxy checks for this before generating the class and logs a warning.
if (Modifier.isFinal(mod)) {
// ...
if (logger.isDebugEnabled()) {
logger.debug("Final method [" + method + "] cannot get proxied via CGLIB: " +
"Calls to this method will NOT be routed to the target instance and " +
"might lead to NPEs against uninitialized fields in the proxy instance.");
}
}
spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java, lines 289–310
"Will NOT be routed to the target instance and might lead to NPEs against uninitialized fields in the proxy instance." Let us see it happen.
// An example written for this post (not Spring's code)
static class Account {
private final String owner;
Account(String owner) { this.owner = owner; }
public String owner() { return owner; } // ordinary method: intercepted
public final String ownerFinal() { return owner; } // final method: cannot be intercepted
}
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new Account("kim")); // no interface, so CGLIB
factory.addAdvice(new LogInterceptor());
Account proxy = (Account) factory.getProxy();
System.out.println("owner() = " + proxy.owner());
System.out.println("ownerFinal() = " + proxy.ownerFinal());
}
-> owner
owner() = kim
ownerFinal() = null
owner() goes through the interceptor to the target and returns kim. ownerFinal() not only has no interceptor log, its value is null. A method that is not overridden runs on the proxy object itself, and the proxy object was created by Objenesis without a constructor call, so its own owner field is empty. This is exactly the situation the warning describes. The whole check is skipped when optimize is on or the log level is above INFO (lines 268–277). The example project runs with the log level at WARN, so the check itself did not run, which is why no warning appears in the output above.
Easy to get wrong
- A
finalclass cannot be wrapped at all. No subclass can be made, so anAopConfigExceptionis thrown. The exception message itself says a final class or a non-visible class is the common cause (lines 232–236). privatemethods cannot be intercepted either. The check skipsstaticandprivatemethods from the start (line 288) because they are not overridable.- JDK proxies do not have this problem. Interface methods cannot be
final. On the other hand, a method that is not on an interface simply does not exist on the proxy.
6. Why calling yourself skips the proxy — self-invocation
Put everything so far together and a conclusion follows naturally. Advice runs only when a method of the proxy is called. In the JDK way the invoke(), and in the CGLIB way the callbacks, are attached to the proxy object; the target object itself is an ordinary object, untouched. So when the target calls this.other() from inside, that call never goes near the proxy; it goes straight to the target's method.
The red box is the path where advice is lost; the blue boxes are the path through the proxy.
The escape hatch Spring provides is exposeProxy and AopContext. In the invoke() of section 3 we saw that with exposeProxy the proxy is stored with AopContext.setCurrentProxy(proxy) and restored in finally. AopContext is a class with a single ThreadLocal, and currentProxy() returns the proxy stored there, or throws if there is none.
public static Object currentProxy() throws IllegalStateException {
Object proxy = currentProxy.get();
if (proxy == null) {
throw new IllegalStateException(
"Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and " +
"ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.");
}
return proxy;
}
spring-aop/src/main/java/org/springframework/aop/framework/AopContext.java, lines 67–75
The Javadoc of setExposeProxy on Advised says the same thing: it can be necessary to expose the proxy if an advised object needs to invoke a method on itself with advice applied; otherwise, if an advised object invokes a method on this, no advice will be applied (lines 76–83). The default is false for performance.
Example ⑤: this.inner() versus currentProxy().inner()
// An example written for this post (not Spring's code)
interface Service {
void outer();
void outerViaProxy();
void inner();
}
static class ServiceImpl implements Service {
public void outer() {
System.out.println(" outer -> this.inner()");
this.inner(); // does not go through the proxy
}
public void outerViaProxy() {
System.out.println(" outerViaProxy -> currentProxy().inner()");
((Service) AopContext.currentProxy()).inner(); // goes through the proxy
}
public void inner() {
System.out.println(" inner");
}
}
public static void main(String[] args) {
System.out.println("[exposeProxy=false]");
ProxyFactory factory = new ProxyFactory(new ServiceImpl());
factory.addAdvice(new LogInterceptor());
Service service = (Service) factory.getProxy();
service.outer();
try {
service.outerViaProxy();
}
catch (IllegalStateException ex) {
System.out.println(" IllegalStateException: " + ex.getMessage());
}
System.out.println("[exposeProxy=true]");
factory.setExposeProxy(true);
Service exposed = (Service) factory.getProxy(); // build a new proxy from the same settings
exposed.outer();
exposed.outerViaProxy();
}
[exposeProxy=false]
-> outer
outer -> this.inner()
inner
-> outerViaProxy
outerViaProxy -> currentProxy().inner()
IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.
[exposeProxy=true]
-> outer
outer -> this.inner()
inner
-> outerViaProxy
outerViaProxy -> currentProxy().inner()
-> inner
inner
Calling outer() prints -> outer, but the inner() called inside has no -> inner. Using currentProxy() without changing the setting throws the exception we saw above. After setExposeProxy(true) and building the proxy again (section 1: "ProxyFactory is configuration"), only currentProxy().inner() prints -> inner. this.inner() is still not advised.
Easy to get wrong
- This is the most common reason
@Transactional"does not work". Calls between methods of the same class do not go through the proxy. The proxies the container builds are the same proxies as in this post, so the same rule applies. Transactions come back in Part 17. AopContextonly works on the same thread. It is aThreadLocal. The exception message says so too.AopContext.currentProxy()is not the recommended approach. The class Javadoc says it should not be used when there is a reasonable alternative, because it makes application code dependent on usage under AOP and on Spring AOP in particular (lines 34–37). The usual fix is to move the method being called into another bean so that the call goes through a proxy.
Left out of this post
A lot was left out to keep the main thread visible. Here they are, named with their location only. The optimisation that pins a chain to each method when the target is static and the config is frozen (FixedChainStaticTargetInterceptor, CglibAopProxy lines 363–391). Dynamic pointcuts that look at the arguments at run time (InterceptorAndDynamicMethodMatcher, ReflectiveMethodInvocation lines 163–175). Introductions, which add interfaces the target does not have to the proxy (IntroductionAdvisor, AdvisedSupport lines 395–401). The adapters that turn advice other than MethodInterceptor (MethodBeforeAdvice and so on) into interceptors (the aop.framework.adapter package). ProxyFactoryBean, which builds a proxy as a bean from XML configuration. Serialisation of proxies and the finer rules of equals. The path that loads pre-generated CGLIB proxy classes under AOT (enhancer.setAttemptLoad, CglibAopProxy line 204). And the return-value conversion for Kotlin suspend functions (CoroutinesUtils).
Recap
| Concept | In one line |
|---|---|
ProxyFactory |
Configuration, not a proxy. Inherits ProxyConfig (flags) → AdvisedSupport (target, interfaces, advisors) → ProxyCreatorSupport (building) |
getProxy() |
Gets an AopProxy from createAopProxy() and calls getProxy(). The AopProxyFactory picks JDK or CGLIB |
| Selection rule | JDK by default. CGLIB if any of optimize, proxyTargetClass or "no user interfaces" holds, except that an interface, JDK proxy or lambda target goes back to JDK |
| JDK dynamic proxy | Proxy.newProxyInstance builds a class implementing the interfaces. Every call arrives at one invoke(). Cannot be cast to the implementation class |
| CGLIB proxy | Enhancer builds a subclass of the target class. Each method gets a callback number. The name contains $$SpringCGLIB$$ |
| Interfaces added automatically | SpringProxy (the stamp), Advised (inspect the configuration), DecoratingProxy (JDK only) |
| Interceptor chain | The list of MethodInterceptors left after filtering the advisors for a method. Cached per method by AdvisedSupport |
ReflectiveMethodInvocation.proceed() |
Bumps the index and calls the next interceptor; at the end, calls the target method by reflection |
| Objenesis | Creates the CGLIB proxy instance without calling a constructor. So the proxy object's fields are empty |
final methods |
Cannot be overridden, so they are not intercepted and run on the proxy object itself. Its fields are empty, so null or NPE |
| Self-invocation | Advice is attached only to the proxy's methods. this.other() never touches the proxy |
exposeProxy + AopContext |
A detour that stores the proxy in a ThreadLocal and hands it out via currentProxy(). Not the recommended way |
What to look at next
In this post we put the target and the interceptor into ProxyFactory by hand. In a real Spring application the container does that work: it decides which beans to wrap, pulls advisors out of classes marked @Aspect, and puts several pieces of advice in order to build the chain. The entry point is AbstractAutoProxyCreator, which was only named in Part 4, and it creates this post's ProxyFactory inside.
- Next: Part 11: How @Aspect Gets Discovered and How the Advice Chain Runs
- Where a proxy becomes a bean (the after-initialisation hook) and early references: Part 4: How a Bean Gets Created — the Creation Flow and the Three-Level Cache
- If AOP and proxies are new words: Part 1: How I Read It with AI, and the Map
- Previous: Part 9: @Conditional, @Profile, @Import — Choosing Beans by Condition