Dynamic Proxies in Spring: JDK Dynamic Proxies vs CGLIB Bytecode Generation
Deep-dive into java.lang.reflect.Proxy, CGLIB Enhancer subclassing, and proxy mechanics in Spring Boot 3
Part 18 in Series — Catch up on the previous article: Aspect-Oriented Programming (AOP) Concepts: JoinPoints, Pointcuts, and Advices (Part 17) before diving into this post.
Why You Need This in Real Life
You write a standard Spring field injection line that you’ve used hundreds of times:
@Autowired
private PaymentService paymentService;
During debugging, you inspect paymentService.getClass().getName() in your IDE evaluator. Instead of returning com.example.service.PaymentServiceImpl, it displays:
com.example.service.PaymentServiceImpl$$SpringCGLIB$$0
Or, if your service implements an interface, it might display:
jdk.proxy2.$Proxy42
When you attempt to cast paymentService to PaymentServiceImpl, your application crashes with a ClassCastException: jdk.proxy2.$Proxy42 cannot be cast to com.example.service.PaymentServiceImpl. Furthermore, when you call a final method on your service, the transaction interceptor fails to trigger entirely.
To prevent cast failures, understand proxy limitations, and debug AOP issues, you must master the fundamental differences between JDK Dynamic Proxies and CGLIB Bytecode Generation.
Part 1: What is a Proxy?
A Proxy is a surrogate object that implements or extends the target class’s API. It intercepts method invocations, executes pre-processing logic (such as starting a database transaction or logging), forwards the invocation to the actual target instance, and performs post-processing.
Caller Method ---> [ Proxy Instance ] ---> Interceptor / Advice ---> [ Target Business Bean ]
Spring IoC container never injects the target bean directly if AOP advice (@Transactional, @Async, @Observed, custom @Aspect) is applied. It injects a runtime-generated proxy object.
Part 2: Strategy 1 - JDK Dynamic Proxies
JDK Dynamic Proxies are built natively into the Java standard library (java.lang.reflect.Proxy).
Core Mechanism
- Requirement: The target class MUST implement at least one interface.
- Generation: Creates a dynamic class extending
java.lang.reflect.Proxyand implementing target interfaces. - Invocation: All method calls are routed to an implementation of
java.lang.reflect.InvocationHandler.
+-----------------------------------+
| java.lang.reflect.Proxy |
+-----------------+-----------------+
|
v
+-----------------------------------+
| jdk.proxy2.$Proxy42 |
| (Implements PaymentService) |
+-----------------+-----------------+
| Delegates to
v
+-----------------------------------+
| InvocationHandler |
| (AOP Advice Interceptor Chain) |
+-----------------+-----------------+
| Forwards call to
v
+-----------------------------------+
| PaymentServiceImpl |
| (Actual Target Bean) |
+-----------------------------------+
Pure JDK Dynamic Proxy Implementation Example
package com.example.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JdkProxyDemo {
public interface OrderService {
void placeOrder(String item);
}
public static class OrderServiceImpl implements OrderService {
@Override
public void placeOrder(String item) {
System.out.println("Executing business logic for order: " + item);
}
}
public static void main(String[] args) {
OrderService target = new OrderServiceImpl();
// Create JDK Dynamic Proxy
OrderService proxy = (OrderService) Proxy.newProxyInstance(
JdkProxyDemo.class.getClassLoader(),
new Class<?>[]{ OrderService.class },
new InvocationHandler() {
@Override
public Object invoke(Object proxyInstance, Method method, Object[] methodArgs) throws Throwable {
System.out.println("[JDK PROXY] Pre-processing: Start Transaction");
Object result = method.invoke(target, methodArgs);
System.out.println("[JDK PROXY] Post-processing: Commit Transaction");
return result;
}
}
);
proxy.placeOrder("Laptop");
}
}
Part 3: Strategy 2 - CGLIB Bytecode Generation
CGLIB (Code Generation Library, repackaged as org.springframework.cglib) generates proxies by dynamically creating a subclass of the target class at runtime using ASM bytecode manipulation.
Core Mechanism
- Requirement: Target class does NOT need to implement an interface.
- Generation: Creates a child class extending the target class (
PaymentServiceImpl$$SpringCGLIB$$0). - Invocation: Overrides all public methods and delegates execution to
org.springframework.cglib.proxy.MethodInterceptor.
+-----------------------------------+
| PaymentServiceImpl |
| (Target Base Class) |
+-----------------+-----------------+
| Subclassed by CGLIB
v
+-----------------------------------+
| PaymentServiceImpl$$SpringCGLIB$0 |
| (Generated Subclass) |
+-----------------+-----------------+
| Intercepts calls via
v
+-----------------------------------+
| MethodInterceptor |
| (AOP Advice Interceptor Chain) |
+-----------------------------------+
Part 4: Comparative Breakdown & Spring Defaults Evolution
| Dimension | JDK Dynamic Proxies | CGLIB Proxies |
|---|---|---|
| Class Hierarchy | Extends java.lang.reflect.Proxy; implements Interfaces. | Subclasses target class (TargetClass$$SpringCGLIB). |
| Interface Requirement | Mandatory. Cannot proxy classes without interfaces. | Optional. Works on concrete classes and interfaces. |
| Final Class / Method Handling | Interfaces cannot be final. | Fails if class or method is final (cannot extend/override). |
| Field Access | Cannot intercept direct field access. | Direct field access on proxy bypasses interceptor (returns null). |
| Spring Boot Pre-2.0 Default | Default when interfaces present. | Default when no interfaces present. |
| Spring Boot 2.x / 3.x Default | Optional (spring.aop.proxy-target-class=false). | Default (spring.aop.proxy-target-class=true). |
Why Spring Boot Changed the Default to CGLIB
In early Spring versions, if a bean implemented an interface, Spring used JDK proxies. However, developers frequently attempted to inject the concrete implementation class instead of the interface:
@Autowired
private PaymentServiceImpl paymentService; // Failed with ClassCastException when JDK Proxy was used!
To eliminate ClassCastException complaints, Spring Boot 2.0+ set spring.aop.proxy-target-class=true by default, using CGLIB proxies across all beans regardless of whether interfaces exist.
Part 5: Production Gotchas & Edge Cases
Gotcha 1: final Classes and Methods in CGLIB
CGLIB generates proxies by creating a subclass and overriding methods. If a class or method is marked final, Java prohibits subclassing and method overriding:
// DANGEROUS IN CGLIB: final method CANNOT be overridden!
@Service
public class SecurityService {
@Transactional
public final void validateToken(String token) {
// CGLIB cannot override validateToken()!
// @Transactional IS SILENTLY IGNORED!
}
}
- Symptom:
@Transactionalor@Aspectadvice fails to trigger onfinalmethods without throwing an exception. - Solution: Remove
finalkeywords from proxied Spring bean classes and methods.
Gotcha 2: Direct Field Access Bypassing Proxies
Because CGLIB proxies are empty subclasses, their inherited fields are uninitialized (null or zero). If external code accesses a field directly instead of calling a getter method (service.myField vs service.getMyField()), it bypasses proxy interceptors and reads the uninitialized null field on the proxy subclass!
- Rule of Thumb: Always encapsulate bean fields behind getter methods.
Next Steps
In Post 19, we will explore externalized configuration, environment property resolution hierarchies, and production observability using Spring Boot Actuator.
References & Further Reading
- Testcontainers Project. Testcontainers Lightweight Throwaway Database Instances. Testcontainers Docs.
- Spring.io. Spring Boot Testing Reference Guide (
@SpringBootTest,@DataJpaTest). Spring Docs. - Smart, J. F. (2014). Java Testing with Spock & JUnit. Manning Publications.
Part 19: Spring Boot Externalized Configuration, Profiles, and Production Observability
Continue to Part 19 →