Proxy Mechanics: JDK Dynamic Proxies vs CGLIB and the Cases Where Each Is Used
Proxy Mechanics: JDK Dynamic Proxies vs CGLIB and the Cases Where Each Is Used
Every Spring feature that intercepts a method call uses a proxy. @Transactional, @Cacheable, @Async, @Validated, security annotations. All of them. If you do not understand how proxies work, you will write code that compiles, passes tests in some configurations, and silently does nothing in production.
This chapter covers the two proxy types Spring uses, how the framework decides between them, and the class hierarchy constraints that make one work and the other fail.
Two Proxy Types, One Goal
Spring proxies exist to insert behavior before, after, or around a method call without modifying the target class source code. There are exactly two mechanisms.
JDK dynamic proxies create a new class at runtime that implements the same interfaces as the target. The proxy is not a subclass of the target. It delegates to an InvocationHandler that dispatches the call to the real object after executing interceptor logic.
CGLIB proxies create a new class at runtime that extends the target class. The proxy is a subclass. It overrides non-final methods and delegates to a MethodInterceptor that dispatches the call to the superclass implementation after executing interceptor logic.
Both produce an object that looks like the target to callers. The difference is the type relationship between the proxy and the target.
How JDK Dynamic Proxies Work
The JDK proxy mechanism lives in java.lang.reflect.Proxy. The factory method Proxy.newProxyInstance takes three arguments: a class loader, an array of interfaces, and an InvocationHandler.
Object proxy = Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
(proxyObj, method, args) -> {
System.out.println("Before: " + method.getName());
Object result = method.invoke(target, args);
System.out.println("After: " + method.getName());
return result;
}
);
The returned object is an instance of a dynamically generated class named something like jdk.proxy2.$Proxy45. This class implements every interface in the array. It is not a subclass of the target class. Every method call on the proxy goes through InvocationHandler.invoke.
The constraint is absolute: the target must implement at least one interface. If it does not, JDK proxying cannot be used.
How CGLIB Proxies Work
CGLIB (Code Generation Library, now bundled inside Spring as spring-core’s org.springframework.cglib package) uses ASM to generate bytecode at runtime. It creates a new class that extends the target class.
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TenantService.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
System.out.println("Before: " + method.getName());
Object result = proxy.invokeSuper(obj, args);
System.out.println("After: " + method.getName());
return result;
});
TenantService proxy = (TenantService) enhancer.create();
The generated class has a name like TenantService$$SpringCGLIB$$0. It extends TenantService. Every non-final, non-private method is overridden with a dispatch that calls the MethodInterceptor callback.
The proxy.invokeSuper(obj, args) call uses a generated FastClass to invoke the superclass method directly, bypassing reflection. This is why CGLIB method dispatch is faster than JDK proxy dispatch after warmup.
Spring Boot’s Default: CGLIB Since Boot 2
Before Spring Boot 2.0, the default was proxyTargetClass=false. Spring would use JDK dynamic proxies when the bean implemented an interface and CGLIB when it did not.
Spring Boot 2.0 changed the default to proxyTargetClass=true. Every proxy-eligible bean gets a CGLIB proxy, regardless of whether it implements interfaces.
The configuration lives in @EnableAspectJAutoProxy(proxyTargetClass = true), which Spring Boot auto-configuration applies via AopAutoConfiguration. You can override it:
# application.properties
spring.aop.proxy-target-class=false
But almost no one does. The CGLIB default eliminated a class of bugs where developers injected a bean by its concrete class type and got a BeanNotOfRequiredTypeException because the JDK proxy only implemented the interface.
The Decision Logic: AbstractAutoProxyCreator
AbstractAutoProxyCreator is the BeanPostProcessor responsible for wrapping beans in proxies. Its postProcessAfterInitialization method checks whether advisors apply to the bean. If they do, it creates a proxy.
The decision flow:
AbstractAutoProxyCreator.postProcessAfterInitialization(bean, beanName)is called.- It calls
wrapIfNecessary(bean, beanName, cacheKey). wrapIfNecessarycallsgetAdvicesAndAdvisorsForBeanto find applicable advisors.- If advisors exist, it calls
createProxy. createProxybuilds aProxyFactory, sets the target, adds advisors, and callsproxyFactory.getProxy().
Inside ProxyFactory.getProxy():
ProxyFactory
→ DefaultAopProxyFactory.createAopProxy(AdvisedSupport)
→ if (config.isOptimize() || config.isProxyTargetClass()
|| hasNoUserSuppliedProxyInterfaces(config))
→ return new ObjenesisCglibAopProxy(config) // CGLIB
else
→ return new JdkDynamicAopProxy(config) // JDK
isProxyTargetClass() returns true by default in Spring Boot. That is why CGLIB wins. The only cases where JDK proxying is used:
proxyTargetClassis explicitly set tofalse- The bean class is an interface itself (rare)
- The
@Scopeannotation usesproxyMode = ScopedProxyMode.INTERFACES
ProxyFactory: The Configuration Object
ProxyFactory is the programmatic API for creating proxies. You rarely use it directly, but understanding it reveals what Spring does internally.
@Component
public class ProxyInspector implements CommandLineRunner {
@Autowired
private TenantService tenantService;
@Override
public void run(String... args) {
// Direct ProxyFactory usage
ProxyFactory factory = new ProxyFactory();
factory.setTarget(new TenantService());
factory.setProxyTargetClass(true); // Force CGLIB
TenantService manualProxy = (TenantService) factory.getProxy();
System.out.println("Manual proxy class: " + manualProxy.getClass().getName());
System.out.println("Is CGLIB: " + AopUtils.isCglibProxy(manualProxy));
}
}
ProxyFactory extends AdvisedSupport, which holds the target source, the advisor chain, and proxy configuration flags. When Spring auto-creates proxies, it constructs a ProxyFactory internally with the same API.
Identifying Proxy Types at Runtime
This is the code you will use in production debugging. Inject any bean and print its proxy information.
@Component
public class ProxyDiagnostics implements CommandLineRunner {
@Autowired private ApplicationContext ctx;
@Override
public void run(String... args) {
String[] beanNames = ctx.getBeanDefinitionNames();
for (String name : beanNames) {
Object bean = ctx.getBean(name);
Class<?> clazz = bean.getClass();
String className = clazz.getName();
if (AopUtils.isAopProxy(bean)) {
boolean isCglib = AopUtils.isCglibProxy(bean);
boolean isJdk = AopUtils.isJdkDynamicProxy(bean);
System.out.printf("Bean: %-40s | CGLIB: %s | JDK: %s | Class: %s%n",
name, isCglib, isJdk, className);
if (isCglib) {
System.out.println(" Superclass: " + clazz.getSuperclass().getName());
}
if (isJdk) {
System.out.println(" Interfaces: " +
java.util.Arrays.toString(clazz.getInterfaces()));
System.out.println(" Is Proxy class: " +
java.lang.reflect.Proxy.isProxyClass(clazz));
}
}
}
}
}
Output in the SaaS backend:
Bean: tenantService | CGLIB: true | JDK: false | Class: com.saas.tenant.TenantService$$SpringCGLIB$$0
Superclass: com.saas.tenant.TenantService
Bean: billingService | CGLIB: true | JDK: false | Class: com.saas.billing.BillingService$$SpringCGLIB$$0
Superclass: com.saas.billing.BillingService
Bean: notificationGateway | CGLIB: true | JDK: false | Class: com.saas.notify.NotificationGatewayImpl$$SpringCGLIB$$0
Superclass: com.saas.notify.NotificationGatewayImpl
The $$SpringCGLIB$$ suffix is the marker. If you see it in a stack trace, you know the call went through a CGLIB proxy.
For JDK proxies (when proxyTargetClass=false):
Bean: notificationGateway | CGLIB: false | JDK: true | Class: jdk.proxy2.$Proxy87
Interfaces: [interface com.saas.notify.NotificationGateway, ...]
Is Proxy class: true
Constraints: What Cannot Be Proxied
Final Classes
CGLIB proxying requires creating a subclass. Final classes cannot be subclassed. Spring will throw an error at startup.
@Service
public final class AuditService {
public void logAction(String tenantId, String action) {
// ...
}
}
If any advisor applies to AuditService, the proxy creation fails:
org.springframework.aop.framework.AopConfigException:
Could not generate CGLIB subclass of class AuditService:
Common causes of this problem include using a final class or a non-visible class
Final Methods: The Silent Failure
This is worse than the final class error because there is no error. The method simply is not intercepted.
// BROKEN: @Cacheable on a final method is silently ignored
@Service
public class TenantConfigService {
@Cacheable("tenantConfigs")
public final TenantConfig getConfig(String tenantId) {
// This method will NEVER be cached.
// CGLIB cannot override final methods.
// No error. No warning. Just a cache miss every time.
return loadConfigFromDatabase(tenantId);
}
private TenantConfig loadConfigFromDatabase(String tenantId) {
System.out.println("Loading config from DB for: " + tenantId);
return new TenantConfig(tenantId);
}
}
The proxy class TenantConfigService$$SpringCGLIB$$0 extends TenantConfigService, but it cannot override getConfig because it is final. The method executes on the superclass directly, never touching the caching interceptor.
There is no log message. No exception. The annotation is present, the proxy exists, and caching does not work.
// CORRECT: Remove the final modifier
@Service
public class TenantConfigService {
@Cacheable("tenantConfigs")
public TenantConfig getConfig(String tenantId) {
return loadConfigFromDatabase(tenantId);
}
private TenantConfig loadConfigFromDatabase(String tenantId) {
System.out.println("Loading config from DB for: " + tenantId);
return new TenantConfig(tenantId);
}
}
CGLIB can now override getConfig. The caching interceptor runs before the method body. First call hits the database. Second call returns from cache.
Private Methods
Private methods are not proxied by either mechanism. JDK proxies only intercept interface methods. CGLIB proxies only override visible methods. Placing @Transactional or @Cacheable on a private method does nothing.
Self-Invocation
When a method on a proxied bean calls another method on the same bean, it calls this.otherMethod(). The this reference is the target object, not the proxy. The interceptor is not invoked. This is covered in detail in CH9.
Verifying Your Understanding
Write a CommandLineRunner that iterates all beans in your application context and prints:
- The bean name
- Whether it is proxied
- The proxy type (CGLIB or JDK)
- The generated class name
- For CGLIB: the superclass chain up to
Object
Then add a final method with @Cacheable and verify that caching does not work by calling the method twice and checking whether the underlying code executes both times. Remove the final modifier and verify that caching works.
This exercise takes fifteen minutes and will save you hours of debugging.
What Comes Next
CH8-S1 covers JDK dynamic proxies in full detail: Proxy.newProxyInstance, InvocationHandler, the type relationship between proxy and target, and why casting to the concrete class fails.
CH8-S2 covers CGLIB proxies in full detail: Enhancer, MethodInterceptor, FastClass, the $$SpringCGLIB$$ naming, and Spring’s objenesis integration that bypasses constructor invocation.
CH9 builds on this foundation to explain AOP advice chains and how multiple interceptors are ordered and executed through the proxy.