The Proxy pattern allows you to add functionality to an original class without modifying its code. This is achieved by introducing a proxy class that acts as an intermediary, enhancing the original class's behavior.
Standard Proxy (Composition)
When the original class and the proxy class implement the same interface, you can substitute the original object with its proxy. This approach adheres to programming based on interfaces rather than concrete implementations.
// Interface defining common operations
public interface IUserService {
UserRecord login(String phoneNumber, String password);
UserRecord register(String phoneNumber, String password);
}
// Original implementation of the service
public class UserServiceImpl implements IUserService {
// ... other attributes and methods omitted ...
@Override
public UserRecord login(String phoneNumber, String password) {
// ... login logic ...
return new UserRecord(/* ... user data ... */);
}
@Override
public UserRecord register(String phoneNumber, String password) {
// ... registration logic ...
return new UserRecord(/* ... user data ... */);
}
}
// Proxy class implementing the same interface
public class UserServiceProxy implements IUserService {
private final MetricsCollector metricsCollector;
private final UserServiceImpl userService; // Delegates to the original service
public UserServiceProxy(UserServiceImpl userService) {
this.userService = userService;
this.metricsCollector = new MetricsCollector();
}
@Override
public UserRecord login(String phoneNumber, String password) {
long startTime = System.currentTimeMillis();
// Delegate the call to the original service
UserRecord userRecord = userService.login(phoneNumber, password);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
RequestInfo requestInfo = new RequestInfo("login", duration, startTime);
metricsCollector.recordRequest(requestInfo);
return userRecord;
}
@Override
public UserRecord register(String phoneNumber, String password) {
long startTime = System.currentTimeMillis();
UserRecord userRecord = userService.register(phoneNumber, password);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
RequestInfo requestInfo = new RequestInfo("register", duration, startTime);
metricsCollector.recordRequest(requestInfo);
return userRecord;
}
}
// Example usage:
// The original service is wrapped by the proxy.
IUserService userService = new UserServiceProxy(new UserServiceImpl());
If the original class does not expose an interface, or if it's a third-party library you cannot modify, you can use inheritance. The proxy class can extend the original class and add its functionality.
// Original class (assuming no interface)
public class UserController {
public UserVo login(String telephone, String password) {
// ... original login logic ...
return new UserVo(/* ... */);
}
public UserVo register(String telephone, String password) {
// ... original register logic ...
return new UserVo(/* ... */);
}
}
// Proxy class extending the original class
public class UserControllerEnhancer extends UserController {
private final MetricsCollector metricsCollector;
public UserControllerEnhancer() {
this.metricsCollector = new MetricsCollector();
}
@Override
public UserVo login(String telephone, String password) {
long startTime = System.currentTimeMillis();
// Call the original method using super
UserVo userVo = super.login(telephone, password);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
RequestInfo requestInfo = new RequestInfo("login", duration, startTime);
metricsCollector.recordRequest(requestInfo);
return userVo;
}
@Override
public UserVo register(String telephone, String password) {
long startTime = System.currentTimeMillis();
UserVo userVo = super.register(telephone, password);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
RequestInfo requestInfo = new RequestInfo("register", duration, startTime);
metricsCollector.recordRequest(requestInfo);
return userVo;
}
}
// Example usage:
UserController enhancedController = new UserControllerEnhancer();
Dynamic Proxy
The standard proxy approach requires reimplementing all methods from the original class in the proxy, leading to repetitive code. Dynamic proxy offers a solution by generating proxy classes at runtime, allowing you to intercept method calls without explicit reimplementation for each method or class.
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// Assumes MetricsCollector and RequestInfo classes are defined elsewhere
public class DynamicMetricsProxy {
private final MetricsCollector metricsCollector;
public DynamicMetricsProxy() {
this.metricsCollector = new MetricsCollector();
}
/**
* Creates a dynamic proxy for the given object.
* @param target The object to proxy.
* @return A proxy object with added metrics collection.
*/
public Object createProxy(Object target) {
Class<?> targetClass = target.getClass();
// Get all interfaces implemented by the target object
Class<?>[] interfaces = targetClass.getInterfaces();
// Create an InvocationHandler to intercept method calls
InvocationHandler handler = new MethodCallHandler(target);
// Generate and return the proxy instance
return Proxy.newProxyInstance(
targetClass.getClassLoader(),
interfaces,
handler
);
}
/**
* Handles method invocations on the proxy.
*/
private class MethodCallHandler implements InvocationHandler {
private final Object proxiedObject;
public MethodCallHandler(Object proxiedObject) {
this.proxiedObject = proxiedObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long startTime = System.currentTimeMillis();
// Invoke the original method on the target object
Object result = method.invoke(proxiedObject, args);
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// Construct a meaningful API name for logging
String apiName = proxiedObject.getClass().getSimpleName() + "." + method.getName();
RequestInfo requestInfo = new RequestInfo(apiName, duration, startTime);
metricsCollector.recordRequest(requestInfo);
return result;
}
}
}
// Example Usage:
// Assume IUserService and UserServiceImpl are defined as in the Standard Proxy section.
DynamicMetricsProxy proxyFactory = new DynamicMetricsProxy();
IUserService userService = (IUserService) proxyFactory.createProxy(new UserServiceImpl());
userService.login("user123", "password"); // Method call will be intercepted
Frameworks like Spring AOP leverage dynamic proxy internally. When you configure AOP aspects, Spring generates dynamic proxies for the target beans. Method calls on these beans are intercepted by the proxy, allowing additional logic (like metrics collection or transaction management) to be executed before or after the original method call.
Application Scenarios for Proxy Pattern
RPC Frameworks Remote Procedure Call (RPC) frameworks, often referred to as Remote Proxies, are a prime example. Libraries like Feign utilize this pattern. A remote proxy hides the complexities of network communication, data serialization/deserialization, and server interaction. Clients can call remote services as if they were local methods, without needing to manage the underlying communication details. Similarly, service developers can focus on business logic without worrying about client interactions.
Caching
The proxy pattern is highly effective for implementing caching mechanisms. For certain requests, if the input parameters are the same and the data hasn't expired, the proxy can return a cached result instead of re-executing the business logic. For instance, you might have a service to retrieve user profile information. A caching proxy could intercept calls to this service. If a request includes a cache-control flag (e.g., ?cached=true), the proxy checks an in-memory cache or a distributed cache like Redis. If a valid cached entry exists, it's returned directly; otherwise, the original service method is called, and its result is stored in the cache before being returned. Dynamic proxies are particularly useful here, especially when integrated with AOP frameworks like Spring, allowing cache logic to be applied declaratively.