Proxy Pattern Explained: Static, JDK Dynamic, and CGLIB Implementations
This article introduces the structural Proxy pattern, demonstrates three implementations—hand‑written static proxy, JDK dynamic proxy, and CGLIB subclass proxy—explains their mechanisms, trade‑offs, and limitations, and shows how Spring AOP selects between them for cross‑cutting concerns like logging, transactions, and caching.
Why a Proxy?
When an order service needs additional concerns such as logging, permission checks, caching, or transaction handling, mixing these cross‑cutting concerns directly into the business method violates the Single Responsibility Principle and leads to duplicated code. The proxy pattern solves this by providing a substitute object that implements the same interface as the real service, allowing extra logic to be inserted before or after delegating to the real implementation.
Static (Hand‑written) Proxy
A static proxy is a class written manually that implements OrderService, holds a reference to the real OrderServiceImpl, and adds logging and timing around the delegated call.
public class OrderServiceProxy implements OrderService {
private final OrderService target;
public OrderServiceProxy(OrderService target) { this.target = target; }
@Override
public void createOrder(long userId) {
System.out.println("[日志] 开始下单,用户:" + userId);
long start = System.currentTimeMillis();
target.createOrder(userId);
System.out.println("[日志] 下单完成,耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}Clients obtain an OrderService reference to the proxy, so the original OrderServiceImpl remains unchanged while logging is applied. This approach is clear and works well when the number of services is small.
Static Proxy Limitations – Class Explosion
If dozens of services (e.g., UserService, ProductService, PaymentService) each require the same logging logic, a separate proxy class must be written for each interface. This leads to massive duplicate code (class explosion) and makes maintenance painful because any change to the logging logic must be applied to every proxy.
JDK Dynamic Proxy
Dynamic proxies generate a proxy class at runtime using the JVM. The only requirement is that the target class implements an interface. The cross‑cutting logic is placed in a single InvocationHandler implementation.
public class LogHandler implements InvocationHandler {
private final Object target;
public LogHandler(Object target) { this.target = target; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[日志] 调用方法:" + method.getName());
long start = System.currentTimeMillis();
Object result = method.invoke(target, args);
System.out.println("[日志] " + method.getName() + " 耗时:" + (System.currentTimeMillis() - start) + "ms");
return result;
}
}
OrderService target = new OrderServiceImpl();
OrderService proxy = (OrderService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LogHandler(target));
proxy.createOrder(1001L);The generated proxy class (e.g., $Proxy0) implements the same interfaces and forwards every method call to the handler, eliminating the need for a separate proxy class per service. However, it can only proxy interfaces.
CGLIB Proxy (Subclass‑based)
When a class does not implement any interface, CGLIB creates a subclass at runtime. The subclass overrides methods and inserts the cross‑cutting logic, then calls the original implementation via MethodProxy.invokeSuper.
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OrderServiceImpl.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("[日志] 调用:" + method.getName());
Object result = proxy.invokeSuper(obj, args);
System.out.println("[日志] 完成");
return result;
}
});
OrderServiceImpl proxy = (OrderServiceImpl) enhancer.create();
proxy.createOrder(1001L);CGLIB works without requiring interfaces but cannot proxy final classes or final methods because they cannot be subclassed or overridden.
Comparison of the Three Proxy Types
Creation time : Static – compile‑time; JDK Dynamic – runtime (JVM generates); CGLIB – runtime (subclass generation).
Mechanism : Static – hand‑written implementation; JDK Dynamic – reflection + interface implementation; CGLIB – inheritance (subclass).
Interface requirement : Static – optional (can use inheritance); JDK Dynamic – must have interfaces; CGLIB – no interface needed.
Main limitations : Static – class explosion, hard to maintain; JDK Dynamic – only interfaces; CGLIB – cannot proxy final classes/methods.
Typical use case : Static – few services, simple logic; JDK Dynamic – target has interfaces; CGLIB – target lacks interfaces.
Spring AOP Integration
Spring AOP automatically chooses the appropriate proxy type: it uses JDK dynamic proxies when the target implements interfaces, otherwise it falls back to CGLIB subclass proxies. The cross‑cutting concerns (logging, transactions, caching, async) are implemented as Advice and applied to join points defined by Pointcut expressions.
A common pitfall is self‑invocation: a method in the same class calling another method bypasses the proxy, so annotations like @Transactional or @Cacheable have no effect. The solution is to invoke the method through the proxy (e.g., by injecting the bean into itself or refactoring the call into another bean).
Proxy vs. Decorator
Both patterns share the same structural skeleton—implement the same interface, hold a reference to the wrapped object, and delegate calls with added behavior. The difference lies in intent: a proxy controls access (e.g., permission checks, lazy loading), while a decorator adds new functionality without altering the original object's contract.
Conclusion
The Proxy pattern provides a way to wrap an existing object with a substitute that can inject cross‑cutting logic without modifying the original code. Three concrete forms exist:
Static proxy : clear but leads to class explosion.
JDK dynamic proxy : runtime generation based on interfaces, solves class explosion but limited to interface‑based targets.
CGLIB proxy : subclass‑based, works for classes without interfaces, but cannot proxy final classes or methods.
Spring AOP combines the first two, automatically picking the suitable implementation, and many Spring features such as @Transactional, @Cacheable, and @Async rely on this proxy mechanism. Understanding the proxy’s mechanics also clarifies the self‑invocation issue and the distinction between proxy and decorator patterns.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
