Proxy Design Pattern: A Practical Implementation Guide
Problem Scenario
Consider an order management system with a specific business requirement: once an order is created, only the order creator should be permitted to modify the order data. All other users must be restricted from making changes.
Basic Implementation Approach
The most straightforward solution involves querying the database and comparing the order's creator identifier with the currently authenticated user's session identifier.
class Order {
private String orderId;
private String creatorId;
private String productInfo;
public Order(String orderId, String creatorId, String productInfo) {
this.orderId = orderId;
this.creatorId = creatorId;
this.productInfo = productInfo;
}
// Getters and setters omitted for brevity
}
Service layer implementation:
class OrderService {
private Map<String, Order> orderStore = new HashMap<>();
public void createOrder(String orderId, String creatorId, String productInfo) {
Order order = new Order(orderId, creatorId, productInfo);
orderStore.put(orderId, order);
}
public void updateOrder(String orderId, String userId, String newInfo) {
Order order = orderStore.get(orderId);
if (order != null) {
if (order.getCreatorId().equals(userId)) {
order.setProductInfo(newInfo);
} else {
System.out.println("Access denied.");
}
} else {
System.out.println("Order not found.");
}
}
}
The Problem with This Approach
The implementation itself is sound and represents a common pattern in Web applications often referred to as the anemic domain model, where business logic resides primarily in service layers through extensive conditional checks. However, as order-related operations grow more complex, the service code becomes increasingly bloated. Initial requirements might only ask for description updates, then product name changes, followed by granular permission rules—and each addition compounds the complexity of the updateOrder method with more conditional branches and setter invocations. This makes the codebase difficult to maintain and extend gracefully.
Proxy Pattern Definition
The Proxy Pattern provides a solution by introducing an intermediary that controls access to another object. This pattern uses object composition to protect or extend functionality rather than modifying the target object directly.
Pattern Structure
Subject: The interface defining the operations that both the proxy and real object must implement.
Proxy: The surrogate object that implements the same interface as the concrete target, maintaining a reference to invoke the actual object when needed. The proxy adds access control and protection logic before delegating calls.
RealSubject: The concrete implementation that performs the actual work.
Implementation:
interface Image {
void render();
}
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromStorage(filename);
}
@Override
public void render() {
System.out.println("Rendering " + filename);
}
private void loadFromStorage(String filename) {
System.out.println("Loading " + filename + " from storage.");
}
}
interface ImageProxy extends Image {
void render();
}
class ProxyImage implements ImageProxy {
private RealImage realImage;
private String filename;
public ProxyImage(String filename) {
this.filename = filename;
}
@Override
public void render() {
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.render();
}
}
public class PatternDemo {
public static void main(String[] args) {
ImageProxy proxy = new ProxyImage("photo.jpg");
proxy.render();
}
}
Practical Implementation
When dealing with order objects, we need to control external access—permitting only authorized users while blocking others. The proxy pattern accomplishes this by wrapping the Order object with an additional layer that handles permission verification. This represents a protective proxy approach.
First, define the order operation interface:
public interface OrderServiceInterface {
String getId();
String getProductName();
String getDetails();
String getCreatedBy();
void setId(String id);
void setDetails(String details);
void setProductName(String name);
void setCreatedBy(String createdBy);
}
The concrete order entity serving as the target object:
class Order implements OrderServiceInterface {
private String id;
private String productName;
private String details;
private String createdBy;
public Order(String id, String productName, String details, String createdBy) {
this.id = id;
this.productName = productName;
this.details = details;
this.createdBy = createdBy;
}
@Override
public String getId() { return id; }
@Override
public String getProductName() { return productName; }
@Override
public String getDetails() { return details; }
@Override
public String getCreatedBy() { return createdBy; }
@Override
public void setId(String id) { this.id = id; }
@Override
public void setProductName(String productName) { this.productName = productName; }
@Override
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
@Override
public void setDetails(String details) { this.details = details; }
}
The proxy implementation:
class OrderProxy implements OrderServiceInterface {
private Order order;
private String currentUserId;
public OrderProxy(Order order, String currentUserId) {
this.order = order;
this.currentUserId = currentUserId;
}
@Override
public String getId() {
return order.getId();
}
@Override
public String getProductName() {
return order.getProductName();
}
@Override
public String getDetails() {
return order.getDetails();
}
@Override
public String getCreatedBy() {
return order.getCreatedBy();
}
@Override
public void setId(String id) {
if (isAuthorized()) {
order.setId(id);
} else {
throw new SecurityException("Only the creator can modify the order ID.");
}
}
@Override
public void setProductName(String productName) {
if (isAuthorized()) {
order.setProductName(productName);
} else {
throw new SecurityException("Only the creator can modify the product name.");
}
}
@Override
public void setCreatedBy(String createdBy) {
throw new UnsupportedOperationException("Changing creator ID is not permitted.");
}
@Override
public void setDetails(String details) {
if (isAuthorized()) {
order.setDetails(details);
} else {
throw new SecurityException("Only the creator can modify the order details.");
}
}
private boolean isAuthorized() {
return order.getCreatedBy() != null
&& order.getCreatedBy().equals(currentUserId);
}
}
Understanding the Proxy Pattern
Characteristics and Classification
The essence of the proxy pattern lies in controlling access to objects. By positioning the proxy between the client and target object, it introduces an intermediary layer that creates substantial flexibility. The proxy can execute additional operations before or after invoking the target object, enabling new functionality or extending existing capabilities. More significantly, the proxy can choose not to instantiate or invoke the target object at all—effectively replacing or completely intercepting the target.
From an implementation perspective, the proxy pattern primarily leverages composition and delegation, which becomes evident in static proxy implementations. However, inheritance-based approaches can also work and may simplify certain scenarios. Using the permission control example, the inheritance approach would look like this:
public class Order {
// Basic order implementation without interface
}
public class OrderProxy extends Order {
public OrderProxy(String productName, int quantity, String orderUser) {
super(productName, quantity, orderUser);
}
public void setProductName(String productName, String user) {
if (user == null || user.equals(this.getOrderUser())) {
super.setProductName(productName);
} else {
System.out.println("Access denied: " + user + " cannot modify the product name.");
}
}
public void setQuantity(int quantity, String user) {
if (user == null || user.equals(this.getOrderUser())) {
super.setQuantity(quantity);
} else {
System.out.println("Access denied: " + user + " cannot modify the quantity.");
}
}
public void setOrderUser(String orderUser, String user) {
if (user == null || user.equals(this.getOrderUser())) {
super.setOrderUser(orderUser);
} else {
System.out.println("Access denied: " + user + " cannot modify the order user.");
}
}
}
Proxy Types
The intermediary layer serves different purposes depending on the proxy type:
Remote Proxy: Hides the fact that an object exists in a different address space. Clients interact with the proxy without needing to know the object's location or network details.
Virtual Proxy: Creates expensive objects on demand, delaying initialization until actually needed. This optimizes performance and conserves resources.
Protection Proxy: Adds access control and business logic around object operatiosn without modifying the target object. This is ideal for encapsulating permission rules and cross-cutting concerns.
Smart Reference: Performs additional operations when an object is accessed, such as reference counting, logging, or lazy loading.
Among these, protection proxies and remote proxies are most commonly used. The order example demonstrates a protection proxy where all business logic remains unchanged while permission handling concentrates in the proxy, effectively isolating volatile requirements and facilitating future extensions.
When to Use the Proxy Pattern
Consider the proxy pattern in these situations:
- Use remote proxy when you need a local representative for an object in a different address space
- Use virtual proxy when object creation is computationally expensiev
- Use protection proxy when you need to control access to the original object
- Use smart reference代理 when additional operations must accompany object access
Java's Built-in Proxy Support
Java provides native proxy support through the java.lang.reflect package, offering a Proxy class and InvocationHandler interface. The manual implementation described above represents static proxying—a significant drawback being that any interface changes require modifications to both the proxy and target classes.
Java's dynamic proxy resolves this limitation. While static proxies must implement every method from the interface, dynamic proxies require only a single invoke method, allowing the proxy to handle interface evolution without changes.
Dynamic proxies in Java can only delegate to interfaces (not classes) and rely on reflection combined with runtime bytecode generation. For class-level proxying, consider cglib or similar libraries.
Here's how to implement the protection proxy using Java's dynamic proxy mechanism:
import java.lang.reflect.*;
public class DynamicProxy implements InvocationHandler {
private OrderServiceInterface targetOrder;
public OrderServiceInterface createProxy(Order order, String userId) {
this.targetOrder = order;
return (OrderServiceInterface) Proxy.newProxyInstance(
order.getClass().getClassLoader(),
order.getClass().getInterfaces(),
this);
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().startsWith("set")) {
String requestingUser = (String) args[0];
if (targetOrder.getCreatedBy() != null
&& targetOrder.getCreatedBy().equals(requestingUser)) {
return method.invoke(targetOrder, args);
} else {
System.out.println("Access denied: " + requestingUser
+ " cannot modify this order data");
}
} else {
return method.invoke(targetOrder, args);
}
return null;
}
}
Usage:
public class Client {
public static void main(String[] args) {
Order order = new Order("ORD-001", 100, "Alice");
DynamicProxy proxyHandler = new DynamicProxy();
OrderServiceInterface orderApi = proxyHandler.createProxy(order, "Alice");
orderApi.setQuantity(150, "Bob");
System.out.println("After Bob's attempt: " + order);
orderApi.setQuantity(150, "Alice");
System.out.println("After Alice's update: " + order);
}
}
The proxy pattern is extensively used throughout Java ecosystems, particularly in Spring's AOP framework, which fundamentally operates on proxy-based mechanisms.