namalzi

← All posts

Digging into Spring's Source 10: How an AOP Proxy Gets Built — the JDK Way and the CGLIB Way

Spring · Published

How a single ProxyFactory.getProxy() call decides between a JDK dynamic proxy and a CGLIB proxy, how each one is built, and how a call on the proxy travels through the interceptor chain to the target object — all checked against the real source. Also why calling your own method from inside the class skips the advice, and the trap with final methods, shown with runnable examples.

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.

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 inheritance chain of ProxyFactory and the path to a proxy. ProxyConfig holds five setting flags, AdvisedSupport holds the TargetSource, the interface list, the advisor list and the per-method interceptor chain cache, ProxyCreatorSupport provides createAopProxy, and ProxyFactory provides getProxy. createAopProxy hands over to an AopProxyFactory, whose default implementation DefaultAopProxyFactory picks either JdkDynamicAopProxy or ObjenesisCglibAopProxy to build the proxy object. 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.

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

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 selection flow of DefaultAopProxyFactory. If optimize or proxyTargetClass is set, or there are no user-supplied interfaces, it heads towards CGLIB: with no target class and no interfaces it throws AopConfigException; if the target class is null, an interface, a JDK proxy class or a lambda class it is JdkDynamicAopProxy; otherwise ObjenesisCglibAopProxy. If the first condition is false it is JdkDynamicAopProxy straight away. The green boxes are the outcomes. Even on the CGLIB road there are three exits back to JDK.

Read top to bottom:

  1. The default is a JDK dynamic proxy. If all three conditions of the first if are false, it is a JdkDynamicAopProxy right away. The three conditions are: optimize is on, proxyTargetClass is on, or there is not a single user-supplied interface.
  2. 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 a JdkDynamicAopProxy after all. Lambda classes are detected by ClassUtils.isLambdaClass (lines 946–949).
  3. 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

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 flow of JdkDynamicAopProxy.invoke. equals and hashCode not declared on an interface are compared between proxies; methods of DecoratingProxy and Advised are answered by the proxy configuration. For any other method: if exposeProxy, store the proxy in AopContext; fetch the target with targetSource.getTarget(); get this method's interceptor chain from the per-method cache. If the chain is empty, call the target method directly via reflection; otherwise go through the interceptors in turn with ReflectiveMethodInvocation.proceed() and call the target method last. If the return value is the target itself, replace it with the proxy. 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.

  1. Special methods are filtered out first. If equals/hashCode are not declared on the interfaces, they are not sent to the target; the proxies are compared with each other. Methods of DecoratingProxy and Advised are answered by the proxy configuration object (this.advised), not the target. That is why ((Advised) proxy).getAdvisors() worked in ①.
  2. If exposeProxy, the proxy is stored in AopContext. We come back to this in section 6.
  3. The target is fetched and this method's interceptor chain is looked up. The subject of section 4.
  4. If the chain is empty, no MethodInvocation is 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, a ReflectiveMethodInvocation is created and proceed() is called.
  5. If the return value is the target itself (this), it is swapped for the proxy. If the target did return 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

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);
		}
	}

spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.java, lines 154–181

The order in which the interceptor chain is walked. The caller calls greet on the proxy; the proxy calls invoke on interceptor 1; when interceptor 1 calls invocation.proceed(), invoke on interceptor 2 is called; when interceptor 2 calls proceed(), invokeJoinpoint() calls greet on the target object. The return value travels back through interceptor 2, interceptor 1 and the proxy to the caller. 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

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.

Callback assignment in a CGLIB proxy. When Enhancer generates a class with the target class as superclass and your interfaces plus SpringProxy and Advised as interfaces, ProxyCallbackFilter.accept picks a callback number per method. finalize gets 2 NO_OVERRIDE, methods of Advised get 4 DISPATCH_ADVISED, equals and hashCode get 5 and 6, a method with advice or a config that is not frozen gets 0 AOP_PROXY, the DynamicAdvisedInterceptor, a method with no advice that could return this gets 1 INVOKE_TARGET, and one that cannot return this gets 3 DISPATCH_TARGET. 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

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.

How self-invocation bypasses the proxy. The caller calls service.outer(), which goes through the proxy and the LogInterceptor to the target's outer(). The this.inner() inside goes straight to the target's inner() without touching the proxy, so the interceptor is not called. With exposeProxy on, calling AopContext.currentProxy().inner() goes through the proxy and the LogInterceptor again before inner() runs. 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

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.

More posts