Skip to main content
spring internals

CGLIB Proxies: Subclass-Based Proxying and Bytecode Generation

9 min read Chapter 24 of 78

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:

  1. Generates bytecode for a new class that extends TenantService.
  2. Overrides every non-final, non-private method.
  3. Each overridden method delegates to the MethodInterceptor callback.
  4. Loads the generated class into the JVM using the target’s class loader.
  5. 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 the this reference inside the generated method.
  • method: the java.lang.reflect.Method being called.
  • args: the method arguments.
  • proxy: a MethodProxy that provides invokeSuper for 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:

  1. Caller invokes createInvoice on the proxy (BillingService$$SpringCGLIB$$0).
  2. The proxy’s DynamicAdvisedInterceptor.intercept is called.
  3. The interceptor invokes the TransactionInterceptor.
  4. The interceptor calls proceed(), which calls invokeSuper.
  5. invokeSuper dispatches to the real BillingService.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.