Spring Boot AOP: Part 3 - CGLib vs JDK Proxies

Two Flavors of Magic

In Part 2, we learned that Spring AOP uses proxies to intercept method calls. But hereโ€™s something interesting: Spring can create proxies in two different ways.

Think of it like having two different tools to accomplish the same job:

  • A Swiss Army knife (CGLib) - works in almost any situation
  • A specialized tool (JDK Proxy) - faster but only works in specific cases

Letโ€™s explore both and understand when to use each.


Type 1: JDK Dynamic Proxies (Interface-Based)

JDK proxies are part of standard Java (since Java 1.3). They work by implementing interfaces.

How It Works

JDK proxies require your class to implement an interface. Spring creates a proxy that implements the same interface.

// Step 1: Define an interface
public interface PaymentService {
    void processPayment(String userId, BigDecimal amount);
    void refundPayment(String transactionId);
}

// Step 2: Implement the interface
@Service
public class PaymentServiceImpl implements PaymentService {
    
    @Override
    public void processPayment(String userId, BigDecimal amount) {
        System.out.println("Processing payment for user: " + userId);
        // business logic
    }
    
    @Override
    public void refundPayment(String transactionId) {
        System.out.println("Refunding transaction: " + transactionId);
        // business logic
    }
}

When Spring creates a proxy, it does something like this:

PaymentService proxy = (PaymentService) Proxy.newProxyInstance(
    classLoader,
    new Class[] { PaymentService.class },
    new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) {
            // Execute @Before advice
            loggingAspect.logBefore();
            
            // Call the actual method
            Object result = method.invoke(actualService, args);
            
            // Execute @After advice
            loggingAspect.logAfter();
            
            return result;
        }
    }
);

The Key Point

The proxy implements the interface, not extends the class. This is important!

PaymentService (interface)
    โ†‘ implements
    |
PaymentServiceImpl (your class)

PaymentService (interface)
    โ†‘ implements
    |
$Proxy0 (JDK proxy)
    โ†’ delegates to โ†’ PaymentServiceImpl

Pros of JDK Proxies

๐Ÿ‘ Part of standard Java - No external dependencies needed

๐Ÿ‘ Lightweight - Minimal overhead

๐Ÿ‘ Fast - Slightly better performance than CGLib

๐Ÿ‘ Clean separation - Encourages interface-based design

Cons of JDK Proxies

๐Ÿ‘Ž Requires an interface - Your class must implement one

๐Ÿ‘Ž Canโ€™t proxy classes directly - Only works with interfaces

๐Ÿ‘Ž Less flexible - More restrictive than CGLib


Type 2: CGLib Proxies (Class-Based)

CGLib (Code Generation Library) is more powerful. It can create proxies for any class, even without an interface.

How It Works

CGLib creates a subclass of your class at runtime using bytecode generation.

// No interface needed!
@Service
public class PaymentService {
    
    public void processPayment(String userId, BigDecimal amount) {
        System.out.println("Processing payment for user: " + userId);
        // business logic
    }
    
    public void refundPayment(String transactionId) {
        System.out.println("Refunding transaction: " + transactionId);
        // business logic
    }
}

CGLib generates a subclass like this:

public class PaymentService$$EnhancerBySpringCGLIB$$a1b2c3d4 extends PaymentService {
    
    private PaymentService target;
    private LoggingAspect loggingAspect;
    
    @Override
    public void processPayment(String userId, BigDecimal amount) {
        // Execute @Before advice
        loggingAspect.logBefore();
        
        // Call the parent method
        super.processPayment(userId, amount);
        
        // Execute @After advice
        loggingAspect.logAfter();
    }
    
    @Override
    public void refundPayment(String transactionId) {
        // Same pattern...
        loggingAspect.logBefore();
        super.refundPayment(transactionId);
        loggingAspect.logAfter();
    }
}

The Key Point

The proxy extends your class. Itโ€™s a subclass!

PaymentService (your class)
    โ†‘ extends
    |
PaymentService$$EnhancerBySpringCGLIB (proxy)

Pros of CGLib Proxies

๐Ÿ‘ Works with any class - No interface required

๐Ÿ‘ More flexible - Can proxy classes directly

๐Ÿ‘ Default in Spring Boot - No configuration needed

๐Ÿ‘ Widely used - Battle-tested in production

Cons of CGLib Proxies

๐Ÿ‘Ž Slightly slower - More overhead than JDK proxies

๐Ÿ‘Ž Canโ€™t proxy final classes - Subclassing wonโ€™t work

๐Ÿ‘Ž Canโ€™t proxy final methods - Canโ€™t override them

๐Ÿ‘Ž Requires CGLib library - Extra dependency (but included in Spring Boot)


Which One Does Spring Use?

By default, Spring Boot uses CGLib proxies for everything.

You can verify this yourself:

@Service
public class PaymentService {
    
    public void processPayment(String userId, BigDecimal amount) {
        // Print the actual class name
        System.out.println("Class: " + this.getClass().getName());
        System.out.println("Simple name: " + this.getClass().getSimpleName());
    }
}

When you run this with AOP enabled, youโ€™ll see:

Class: com.app.service.PaymentService$$EnhancerBySpringCGLIB$$a1b2c3d4
Simple name: PaymentService$$EnhancerBySpringCGLIB$$a1b2c3d4

See that $$EnhancerBySpringCGLIB? Thatโ€™s your proofโ€”itโ€™s a CGLib proxy!

Fun fact: The random characters at the end (a1b2c3d4) are generated by CGLib to ensure unique class names. Each proxy gets a different suffix.


Switching to JDK Proxies

If you prefer JDK proxies (and your classes implement interfaces), you can configure Spring to use them:

# application.properties
spring.aop.proxy-target-class=false

Or in YAML:

# application.yml
spring:
  aop:
    proxy-target-class: false

Important: If you set proxy-target-class=false but your class doesnโ€™t implement an interface, Spring will throw an error at startup:

Cannot create JDK dynamic proxy: target class does not implement any interfaces

Make sure all your AOP-enabled classes implement interfaces if you use this setting!


Side-by-Side Comparison

FeatureCGLib ProxyJDK Proxy
Requires interface?๐Ÿšซ Noโœ”๏ธ Yes
How it worksSubclassing (extends)Interface implementation
Can proxy final classes?๐Ÿšซ NoN/A
Can proxy final methods?๐Ÿšซ Noโœ”๏ธ Yes (on interface)
PerformanceSlightly slowerSlightly faster
Memory usageSlightly higherSlightly lower
Spring Boot defaultโœ”๏ธ Yes๐Ÿšซ No
ConfigurationDefaultproxy-target-class=false
External dependencyCGLib (included)None (JDK built-in)
Use caseGeneral purposeInterface-based design


Practical Example: Seeing Proxies in Action

Letโ€™s create a simple example to see proxies at work:

@Service
public class OrderService {
    
    public void createOrder(String userId, List<Item> items) {
        System.out.println("=== Inside createOrder ===");
        System.out.println("Class: " + this.getClass().getSimpleName());
        
        Order order = new Order(userId, items);
        order.calculateTotal();
        
        System.out.println("Order created: " + order.getId());
    }
}

@Aspect
@Component
public class LoggingAspect {
    
    @Before("execution(* com.app.service.OrderService.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("โ†’ [BEFORE] Calling: " + joinPoint.getSignature().getName());
    }
    
    @AfterReturning("execution(* com.app.service.OrderService.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("โ† [AFTER] Finished: " + joinPoint.getSignature().getName());
    }
}

When you call orderService.createOrder(), youโ€™ll see:

โ†’ [BEFORE] Calling: createOrder
=== Inside createOrder ===
Class: OrderService$$EnhancerBySpringCGLIB$$a1b2c3d4
Order created: ORD-12345
โ† [AFTER] Finished: createOrder

Notice:

  1. The @Before advice runs first
  2. The actual method executes
  3. The class name shows itโ€™s a CGLib proxy
  4. The @After advice runs last

When to Use Which Proxy?

Recommendation: Stick with the default (CGLib) unless you have a specific reason to change. It works in 99% of cases and requires less boilerplate.


Key Takeaways

  • Spring supports two proxy types: CGLib (class-based) and JDK (interface-based)
  • CGLib is the default and works with any class
  • JDK proxies require interfaces but are slightly faster
  • Only public methods can be effectively proxied
  • Final classes/methods canโ€™t be proxied by CGLib
  • Understanding proxy limitations helps you write better AOP code

๐Ÿ”ฌ Want to See How Proxies Work Internally?

Curious about how JDK Dynamic Proxies and CGLib proxies actually work under the hood? Iโ€™ve created a hands-on demo project that shows you the internals!

๐Ÿ“ฆ Demo Project: Spring Dynamic Proxy Demo

This repository contains working examples that demonstrate:

  • How JDK Dynamic Proxies create proxy instances
  • How CGLib generates subclasses at runtime
  • Side-by-side comparison of both approaches
  • Real code you can run and experiment with

Perfect for understanding the low-level mechanics behind Spring AOP!


Whatโ€™s Next?

Letโ€™s write some code !


Comments

Join the discussion and share your thoughts