CGLIB Proxies: Subclass-Based Proxying and Bytecode Generation
CGLIB Proxies: Subclass-Based Proxying and Bytecode Generation
CGLIB proxying is Spring Boot’s default. Every @Service, @Component, @Repository, and @Configuration class that requires interception gets a CGLIB proxy. Understanding this mechanism means understanding what most of your beans actually are at runtime: not instances of your class, but instances of a generated subclass.
The Enhancer: CGLIB’s Proxy Factory
CGLIB’s Enhancer is the equivalent of Proxy.newProxyInstance for subclass-based proxies. It generates a new class that extends the target.
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TenantService.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, methodProxy) -> {
System.out.println("[INTERCEPT] " + method.getName());
Object result = methodProxy.invokeSuper(obj, args);
return result;
});
TenantService proxy = (TenantService) enhancer.create();
The Enhancer does the following:
- Generates bytecode for a new class that extends
TenantService. - Overrides every non-final, non-private method.
- Each overridden method delegates to the
MethodInterceptorcallback. - Loads the generated class into the JVM using the target’s class loader.
- Instantiates the generated class.
The result is a real Java class, loadable and inspectable like any other. It just happens to have been created at runtime instead of compiled from source.
The MethodInterceptor Callback
MethodInterceptor is CGLIB’s dispatch interface:
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable;
}
Four parameters:
obj: the proxy instance (the subclass). This is thethisreference inside the generated method.method: thejava.lang.reflect.Methodbeing called.args: the method arguments.proxy: aMethodProxythat providesinvokeSuperfor calling the original method.
The critical difference from JDK’s InvocationHandler is MethodProxy. It does not use Method.invoke (reflection). It uses a generated FastClass that dispatches by method index, calling the superclass method directly. This avoids the overhead of reflection.
Spring’s ObjenesisCglibAopProxy sets up the MethodInterceptor to execute the AOP advisor chain, then call methodProxy.invokeSuper for the actual method body.
The $$SpringCGLIB$$ Naming Convention
Spring’s CGLIB integration uses a specific naming scheme. The generated class name follows this pattern:
{OriginalClassName}$$SpringCGLIB$${index}
Examples:
com.saas.tenant.TenantService$$SpringCGLIB$$0
com.saas.billing.BillingService$$SpringCGLIB$$0
com.saas.config.AppConfig$$SpringCGLIB$$0
The $$SpringCGLIB$$ marker distinguishes Spring-generated proxies from other CGLIB users. The trailing index (0, 1, …) differentiates multiple proxy classes for the same target (uncommon but possible with multiple proxy layers).
Before Spring Framework 4.0, the marker was $$EnhancerBySpringCGLIB$$. The shorter form was adopted when Spring embedded CGLIB directly into spring-core.
Bytecode Generation: What Gets Created
When CGLIB generates the subclass, the bytecode contains:
Overridden methods. For each non-final, non-private method in the superclass, the generated class has an override that dispatches to the callback. The generated bytecode is equivalent to:
// Generated (conceptual, not actual source)
public class TenantService$$SpringCGLIB$$0 extends TenantService {
private MethodInterceptor callback;
@Override
public TenantConfig getConfig(String tenantId) {
// Dispatch to MethodInterceptor
return (TenantConfig) callback.intercept(
this,
TenantService.class.getMethod("getConfig", String.class),
new Object[]{ tenantId },
CGLIB$getConfig$0$Proxy // MethodProxy for invokeSuper
);
}
// Bridge method for super call
final TenantConfig CGLIB$getConfig$0(String tenantId) {
return super.getConfig(tenantId);
}
}
FastClass instances. Two FastClass classes are generated: one for the proxy class, one for the superclass. FastClass maps method signatures to integer indices and dispatches calls by index, avoiding Method.invoke.
Static initializer. The generated class has a CGLIB$STATICHOOK method that initializes Method objects, MethodProxy instances, and FastClass references. This runs once when the class is loaded.
Inspecting CGLIB Proxies at Runtime
The SaaS backend’s diagnostic bean:
@Component
public class CglibProxyInspector implements CommandLineRunner {
@Autowired private ApplicationContext ctx;
@Override
public void run(String... args) {
for (String name : ctx.getBeanDefinitionNames()) {
Object bean = ctx.getBean(name);
if (AopUtils.isCglibProxy(bean)) {
Class<?> proxyClass = bean.getClass();
Class<?> targetClass = AopUtils.getTargetClass(bean);
System.out.printf("Bean: %s%n", name);
System.out.printf(" Proxy class: %s%n", proxyClass.getName());
System.out.printf(" Target class: %s%n", targetClass.getName());
System.out.printf(" Is subclass: %s%n",
targetClass.isAssignableFrom(proxyClass));
// Print the superclass chain
System.out.print(" Class chain: ");
Class<?> current = proxyClass;
while (current != null) {
System.out.print(current.getSimpleName());
current = current.getSuperclass();
if (current != null) System.out.print(" -> ");
}
System.out.println();
// Print advisors
if (bean instanceof Advised advised) {
Advisor[] advisors = advised.getAdvisors();
System.out.printf(" Advisors: %d%n", advisors.length);
for (Advisor advisor : advisors) {
System.out.printf(" - %s%n",
advisor.getClass().getSimpleName());
}
}
System.out.println();
}
}
}
}
Output for the SaaS backend:
Bean: tenantService
Proxy class: com.saas.tenant.TenantService$$SpringCGLIB$$0
Target class: com.saas.tenant.TenantService
Is subclass: true
Class chain: TenantService$$SpringCGLIB$$0 -> TenantService -> Object
Advisors: 1
- BeanFactoryCacheOperationSourceAdvisor
Bean: billingService
Proxy class: com.saas.billing.BillingService$$SpringCGLIB$$0
Target class: com.saas.billing.BillingService
Is subclass: true
Class chain: BillingService$$SpringCGLIB$$0 -> BillingService -> Object
Advisors: 2
- BeanFactoryTransactionAttributeSourceAdvisor
- BeanFactoryCacheOperationSourceAdvisor
The key observation: Is subclass: true. Unlike JDK proxies, a CGLIB proxy IS-A instance of the target class. You can inject by concrete type without BeanNotOfRequiredTypeException.
Objenesis: Bypassing the Constructor
Early CGLIB required the target class to have a no-arg constructor. Enhancer.create() called the constructor to instantiate the proxy. If the constructor had required arguments, proxy creation failed.
Spring solved this with Objenesis. ObjenesisCglibAopProxy uses the Objenesis library to instantiate the proxy class without calling any constructor. It uses JVM-internal mechanisms (sun.misc.Unsafe.allocateInstance or serialization tricks) to allocate the object directly.
@Service
public class BillingService {
private final TenantRepository tenantRepo;
private final PaymentGateway paymentGateway;
// No no-arg constructor. Only this parameterized constructor.
public BillingService(TenantRepository tenantRepo, PaymentGateway paymentGateway) {
this.tenantRepo = tenantRepo;
this.paymentGateway = paymentGateway;
}
@Transactional
public Invoice createInvoice(String tenantId, BigDecimal amount) {
// ...
}
}
Without Objenesis, CGLIB would fail because there is no no-arg constructor. With Objenesis, the proxy instance is allocated without constructor invocation. The fields are null in the proxy object, but that does not matter because method calls are dispatched to the real bean instance (which was created normally by the Spring container with proper constructor injection).
This is subtle. The proxy object and the target object are separate instances. The proxy’s fields are uninitialized. The proxy’s methods delegate to the target through the MethodInterceptor. The target was created normally with all constructor arguments.
Constraint: Final Classes
CGLIB creates a subclass. Java forbids extending a final class. If a proxy-eligible bean is final, startup fails.
// BROKEN: Proxy creation fails at startup
@Service
@CacheConfig(cacheNames = "tenants")
public final class TenantLookupService {
@Cacheable
public Tenant findById(String tenantId) {
return tenantRepository.findById(tenantId).orElseThrow();
}
}
Error:
org.springframework.aop.framework.AopConfigException:
Could not generate CGLIB subclass of class
com.saas.tenant.TenantLookupService:
Common causes include using a final class or a non-visible class.
This is a hard error. The application does not start.
The Kotlin Problem
Kotlin classes are final by default. Every Kotlin class without the open modifier cannot be CGLIB-proxied.
// BROKEN: Kotlin data class is final, cannot be proxied
@Service
class PricingService(private val repo: PricingRepository) {
@Cacheable("prices")
fun getPrice(tenantId: String, productId: String): BigDecimal {
return repo.findPrice(tenantId, productId)
}
}
The Kotlin compiler generates PricingService as a final class. CGLIB cannot subclass it. The application crashes at startup.
// CORRECT: Mark the class as open
@Service
open class PricingService(private val repo: PricingRepository) {
@Cacheable("prices")
open fun getPrice(tenantId: String, productId: String): BigDecimal {
return repo.findPrice(tenantId, productId)
}
}
Both the class and the method must be open. The kotlin-spring compiler plugin (kotlin-allopen) automates this for classes annotated with @Component, @Service, @Repository, @Controller, @Configuration, and @Transactional. If you use Spring with Kotlin without the plugin, expect proxy failures.
Constraint: Final Methods
Final methods on non-final classes do not cause an error. CGLIB creates the subclass, but it cannot override the final method. The method runs directly on the superclass implementation, bypassing all interceptors.
@Service
public class UsageTracker {
// This method is proxied normally
@Cacheable("usage")
public UsageReport getUsage(String tenantId) {
return computeUsage(tenantId);
}
// BROKEN: final method, silently not intercepted
@Cacheable("quotas")
public final QuotaStatus getQuota(String tenantId) {
return computeQuota(tenantId);
}
}
getUsage is cached. getQuota is not. No error, no warning. The @Cacheable annotation on getQuota is decoration with no effect.
To detect this, check your codebase for final methods that carry Spring annotations:
@Component
public class FinalMethodDetector implements CommandLineRunner {
@Autowired private ApplicationContext ctx;
@Override
public void run(String... args) {
for (String name : ctx.getBeanDefinitionNames()) {
Object bean = ctx.getBean(name);
if (!AopUtils.isCglibProxy(bean)) continue;
Class<?> targetClass = AopUtils.getTargetClass(bean);
for (Method method : targetClass.getDeclaredMethods()) {
if (java.lang.reflect.Modifier.isFinal(method.getModifiers())) {
boolean hasSpringAnnotation =
method.isAnnotationPresent(
org.springframework.cache.annotation.Cacheable.class) ||
method.isAnnotationPresent(
org.springframework.transaction.annotation.Transactional.class) ||
method.isAnnotationPresent(
org.springframework.scheduling.annotation.Async.class);
if (hasSpringAnnotation) {
System.out.printf(
"WARNING: %s.%s() is final but has Spring annotations. " +
"Interceptors will NOT run.%n",
targetClass.getSimpleName(), method.getName());
}
}
}
}
}
}
Run this at startup in development. It costs milliseconds and catches bugs that cost hours.
Package-Private and Protected Methods
CGLIB can intercept package-private and protected methods because the generated subclass is in the same package as the target (by default). Spring configures CGLIB to place the generated class in the target’s package.
@Service
public class TenantProvisioner {
// Package-private: intercepted by CGLIB
@Transactional
void provisionTenant(String tenantId) {
// ...
}
// Protected: intercepted by CGLIB
@Transactional
protected void deprovisionTenant(String tenantId) {
// ...
}
}
Both methods will be proxied. JDK proxies would not intercept either because they are not declared on any interface.
Debugging CGLIB Proxies in the IDE
When you set a breakpoint in a proxied method and step through it, the call stack shows the CGLIB dispatch:
createInvoice:42, BillingService (com.saas.billing)
invokeSuper:258, MethodProxy (org.springframework.cglib.proxy)
proceed:78, CglibMethodInvocation (org.springframework.aop.framework)
invoke:121, TransactionInterceptor (org.springframework.transaction.interceptor)
intercept:97, DynamicAdvisedInterceptor (org.springframework.aop.framework)
createInvoice:-1, BillingService$$SpringCGLIB$$0 (com.saas.billing)
Read the stack bottom-up:
- Caller invokes
createInvoiceon the proxy (BillingService$$SpringCGLIB$$0). - The proxy’s
DynamicAdvisedInterceptor.interceptis called. - The interceptor invokes the
TransactionInterceptor. - The interceptor calls
proceed(), which callsinvokeSuper. invokeSuperdispatches to the realBillingService.createInvoice.
The :-1 line number on the CGLIB class means the bytecode has no source file. This is expected. Step through it and you land in the real method.
Key Takeaways
CGLIB proxies are subclasses. They inherit the target’s type, which is why injection by concrete type works. Objenesis handles constructors. Final classes break at startup. Final methods fail silently. The $$SpringCGLIB$$ naming is your signal in stack traces and logs. Use AopUtils.isCglibProxy() and the Advised interface to inspect proxy state at runtime. In Kotlin, use the kotlin-spring plugin or mark classes and methods open manually.