The Proxy Pattern in Java
Where the proxy pattern is used
- AOP: a proxy class intercepts calls to the target object’s methods and enhances them.
- Access control: a proxy class restricts access to certain methods or resources.
- Logging: record log information when the target object’s methods are called.
- Performance monitoring: measure method execution time in the proxy to find performance bottlenecks.
- Lazy loading: defer initialization of certain resources through a proxy object to save memory or improve performance.
- Remote proxy: provide a local proxy for remote calls, so calling a local method feels like calling a remote service.
Why introduce a proxy
Implementing features like logging or performance monitoring in a proxy, rather than modifying the source code directly, effectively decouples those features from the business logic.
Single responsibility principle:
Put logging, performance monitoring, and the like in the proxy, and let the target class focus on business logic. This weakens the dependency between the business logic and cross-cutting concerns, in line with the single responsibility principle. If logging or monitoring requirements change later, say the log format, level, or storage, you only modify the proxy class instead of touching every target class.
Code reuse and consistency:
Reuse: one proxy class can serve multiple target classes, so you don’t reimplement the same feature (like logging) in each one, reducing redundant code.
Consistency: the same logging or monitoring logic applies across all target classes; every log follows the same format, which makes later analysis and monitoring easier.
Easy to extend and modify
Extension: to add new features (exception handling, input validation, and so on), implement them in the proxy without disturbing the target class’s business logic. For example, you can add performance monitoring on top of logging just by extending the invoke method.
Flexibility: you can decide at runtime which proxy to use. Different proxy implementations can add different features without modifying the target class, which also reduces conflicts when working in a team.
In Java, the proxy pattern comes in two main flavors: static proxies and dynamic proxies. Both control access to the target object, but they are implemented differently.
Static Proxy
Suppose you have an interface BankAccount that defines 3 methods, one of which is withdraw().
Suppose a class BankAccountImpl implements this interface and handles the actual withdrawal. You can then create a proxy class BankAccountProxy that also implements the interface, but adds extra operations before and after calling BankAccountImpl’s withdraw method, such as logging or permission checks.
Steps
- Define the interface,
BankAccount Interface - Define a class implementing it,
BankAccountImpl implements BankAccount - Write the proxy class,
BankAccountProxy - Call
BankAccountProxy’s constructor to create a static proxy instance
package proxydemo;// Account service interface defining financial operationspublic interface BankAccount { void viewBalance(); // view balance void withdraw(); // withdraw void deposit(); // deposit}package proxydemo;// Account service implementationpublic class BankAccountImpl implements BankAccount { @Override public void viewBalance() { System.out.println("Displaying account balance."); } @Override public void withdraw() { System.out.println("Withdrawing funds from account."); } @Override public void deposit() { System.out.println("Depositing funds into account."); }}package proxydemo;// Account service proxy class// The proxy controls access to the real account service operations.// Only the ADMIN role may perform withdraw(), deposit() and transfer(),// while regular users can only view the balance and exit the service.public class BankAccountProxy implements BankAccount { private BankAccountImpl BankAccount; private String userRole; // Constructor taking the real account service object and the user role public BankAccountProxy(BankAccountImpl BankAccount, String userRole) { this.BankAccount = BankAccount; this.userRole = userRole; System.out.println("BankAccountProxy initialized with user role: " + userRole); } @Override public void viewBalance() { System.out.println("User with role " + userRole + " is viewing balance."); BankAccount.viewBalance(); // every user may view the balance } @Override public void withdraw() { if ("ADMIN".equals(userRole)) { BankAccount.withdraw(); // only admins may withdraw } else { System.err.println("Access denied for user with role " + userRole + ": Only ADMIN can withdraw funds."); } } @Override public void deposit() { if ("ADMIN".equals(userRole)) { BankAccount.deposit(); // only admins may deposit } else { System.err.println("Access denied for user with role " + userRole + ": Only ADMIN can deposit funds."); } }}package proxydemo;public class Main { public static void main(String[] args) { BankAccountImpl BankAccount = new BankAccountImpl(); // Create a proxy for an admin user System.out.println("Admin user:"); BankAccount adminProxy = new BankAccountProxy(BankAccount, "ADMIN"); adminProxy.viewBalance(); // admin can view balance adminProxy.withdraw(); // admin can withdraw adminProxy.deposit(); // admin can deposit // Create a proxy for a regular user System.out.println("\nRegular user:"); BankAccount userProxy = new BankAccountProxy(BankAccount, "USER"); userProxy.viewBalance(); // regular user can view balance userProxy.withdraw(); // regular user cannot withdraw userProxy.deposit(); // regular user cannot deposit }}Admin user:BankAccountProxy initialized with user role: ADMINUser with role ADMIN is viewing balance.Displaying account balance.Withdrawing funds from account.Depositing funds into account.Regular user:BankAccountProxy initialized with user role: USERUser with role USER is viewing balance.Displaying account balance.Access denied for user with role USER: Only ADMIN can withdraw funds.Access denied for user with role USER: Only ADMIN can deposit funds.Pros:
- You can add new features without modifying the
BankAccountImplclass; coupling between the proxy class and the target class stays low.
Cons:
- If the target class has many methods, writing the proxy class by hand is tedious.
- The proxy class carries a lot of code, and every change has to be maintained by hand.
Dynamic Proxy
A static proxy is fixed at compile time: the proxy class’s bytecode is already determined, and every class needs its own proxy class, which leads to redundant code. A dynamic proxy skips writing each proxy class by hand; the proxy class is generated at runtime and written to a class file, so there is less redundancy than with a static proxy.
Java offers two main kinds of dynamic proxy:
- JDK dynamic proxy: interface-based; it can only proxy classes that implement an interface.
- CGLIB dynamic proxy: inheritance-based; it can proxy classes that implement no interface.
JDK Dynamic Proxy
JDK dynamic proxies are built on java.lang.reflect.Proxy and InvocationHandler.
- When a method on the proxy class is called, the call is delegated to the
invokemethod of theInvocationHandlerinstance. - The
Proxyclass creates the proxy object via reflection, based on the interfaces the target class implements and theInvocationHandlerinterface.
I won’t go into how JDK dynamic proxies work under the hood here; if you’re curious, see 彻底明白 JDK 动态代理的底层原理.
Steps
- Define the interface,
BankAccount Interface - Define a class implementing it,
BankAccountImpl implements BankAccount - Create an implementation of the
InvocationHandlerinterface - Use
java.lang.reflect.Proxy’snewProxyInstancemethod to create a dynamic proxy instance
package proxydemo;// Account service interface defining financial operationspublic interface BankAccount { void deposit(double amount); void withdraw(double amount); double getBalance();}package proxydemo;// Account service implementationpublic class BankAccountImpl implements BankAccount { private double balance; @Override public void deposit(double amount) { balance += amount; System.out.println("Deposited: " + amount); } @Override public void withdraw(double amount) { if (balance >= amount) { balance -= amount; System.out.println("Withdrew: " + amount); } else { System.out.println("Insufficient balance"); } } @Override public double getBalance() { return balance; }}Defining the JDK dynamic proxy class
InvocationHandler is an interface in Java’s reflection package java.lang.reflect that handles method calls on proxy instances.
import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;public class BankAccountProxyHandler implements InvocationHandler { private final BankAccount target; public BankAccountProxyHandler(BankAccount target) { this.target = target; } /** * Triggered when a method is invoked on the proxy instance. It receives three arguments: the proxy instance, the invoked Method object, and the argument array. This method handles the call and returns the result. * * @param proxy the proxy object itself; usually not used directly inside the method. * @param method the target method being invoked; a Method object wrapping its metadata (name, parameter types, etc.). * @param args the actual arguments passed to the target method, as an Object[] array; null if there are none. * @return the method's return value (if it has one). * @throws Throwable rethrown if invoking the target object's method throws. */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // You can do work before invoking the method; // for example, add log output or record the start time before the target method runs long startTime = System.currentTimeMillis(); System.out.println("Method " + method.getName() + " is called with arguments: " + (args != null ? java.util.Arrays.toString(args) : "[]")); // Use the Method object to invoke the target (real) object's method, passing args Object result = method.invoke(target, args); // Likewise, you can do work after the method call; // for example, add a log entry afterwards long endTime = System.currentTimeMillis(); System.out.println("Method " + method.getName() + " executed in " + (endTime - startTime) + " ms"); // Return the result (the method's return value if any, otherwise null) return result; }}The Proxy class is a key class in the Java reflection API, in the java.lang.reflect package. Its main job is to create proxy objects dynamically, that is, to generate at runtime a proxy class implementing given interfaces.
Proxy.newProxyInstance is the core method for creating dynamic proxy objects. It takes three arguments:
- A class loader
ClassLoader: the class loader used to load the proxy class, usually the target class’s class loader. - An interface array
Class<?>[]: the interfaces the proxy class should implement. The proxy object will implement all methods in these interfaces. - An
InvocationHandlerinstance: an InvocationHandler responsible for handling method calls on the proxy object.
package proxydemo;import java.lang.reflect.Proxy;public class Main { public static void main(String[] args) { // Create the real bank account object BankAccount realAccount = new BankAccountImpl(); // Create the proxy object BankAccount proxyAccount = (BankAccount) JdkProxyFactory.getProxy(realAccount); // Call methods through the proxy proxyAccount.deposit(100); proxyAccount.withdraw(50); System.out.println("Balance: " + proxyAccount.getBalance()); }}class JdkProxyFactory { public static Object getProxy(Object target) { // target: real object which implements BankAccount Interface // Create the dynamic proxy object via the Proxy class // arg 1: class loader, usually the target object's class loader // arg 2: interfaces implemented by the target object; multiple allowed // arg 3: the InvocationHandler implementation return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new BankAccountProxyHandler((BankAccount) target)); }}Method deposit is called with arguments: [100.0]Deposited: 100.0Method deposit executed in 20 msMethod withdraw is called with arguments: [50.0]Withdrew: 50.0Method withdraw executed in 1 msMethod getBalance is called with arguments: []Method getBalance executed in 3 msBalance: 50.0CGLIB Dynamic Proxy
CGLIB (Code Generation Library) relies on the ASM library, letting us modify and generate bytecode at runtime. CGLIB implements proxying through inheritance.
CGLIB is used within the Spring framework.
Spring AOP uses either JDK dynamic proxies or CGLIB to create the proxy for a given target object. JDK dynamic proxies are built into the JDK, whereas CGLIB is a common open-source class definition library (repackaged into spring-core).
If the target object to be proxied implements at least one interface, a JDK dynamic proxy is used. All of the interfaces implemented by the target type are proxied. If the target object does not implement any interfaces, a CGLIB proxy is created.
CGLIB vs. JDK dynamic proxy
- What can be proxied:
- JDK dynamic proxy: only classes that implement an interface.
- CGLIB dynamic proxy: plain classes (with no interface).
- How it works:
- JDK dynamic proxy: via reflection; the proxy class implements the target object’s interfaces.
- CGLIB dynamic proxy: by generating a subclass of the target class.
- Limitations:
- CGLIB cannot proxy final classes or final methods, because it works by generating subclasses and final methods cannot be overridden.
Steps
- Add the CGLIB dependency
- Create the target class,
BankAccount - Create a MethodInterceptor implementation to handle the interception logic
- Generate the proxy object: create the proxy instance via CGLIB
public class BankAccount { public void deposit(double amount) { System.out.printf("Depositing: " + amount); } public void withdraw(double amount) { System.out.printf("Withdrawing: " + amount); }}import net.sf.cglib.proxy.MethodInterceptor;import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;public class BankAccountInterceptor implements MethodInterceptor { /** * Interceptor method * * @param proxyObj the proxy object * @param method the proxied method * @param args the method arguments * @param proxy the method's proxy, used to invoke the superclass method * @return the method's return value * @throws Throwable any exception thrown */ @Override public Object intercept(Object proxyObj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("Method" + method.getName() + "is being called with arguments: " + java.util.Arrays.toString(args)); Object res = proxy.invoke(proxyObj, args); System.out.println("Method" + method.getName() + "execution completed"); return res; }}import net.sf.cglib.proxy.Enhancer;public class Main { public static void main(String[] args) { // Create the dynamic proxy class Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(BankAccount.class); enhancer.setCallback(new BankAccountInterceptor()); // Create the proxy object BankAccount proxyAccount = (BankAccount)enhancer.create(); // Call methods through the proxy proxyAccount.deposit(100); proxyAccount.withdraw(50); }}Method deposit is being called with arguments: [100.0]Depositing: 100.0Method deposit execution completed.Method withdraw is being called with arguments: [50.0]Withdrawing: 50.0Method withdraw execution completed.


