Proxy Pattern: Providing an Interface to Control the Behavior of Other Objects
In complex software design systems, directly invoking the internal implementation of target objects often leads to high coupling and difficult extensibility. The Proxy Pattern, a core strategy within behavioral design patterns, aims to provide a surrogate or intermediary layer for other objects, thereby controlling access permissions to targets and enhancing their functionality without altering the original class structure. In short, the essence of the Proxy Pattern lies in "indirect invocation." It introduces an intermediary that replaces direct operations on real objects, protecting loose coupling between clients and concrete implementations while enabling flexible injection of cross-cutting concerns (such as logging or permission verification).
Based on the method of control, Proxy Patterns are primarily divided into Static Proxies and Dynamic Proxies. The former is most common in basic teaching, while the latter offers higher flexibility. Understanding the distinction between these two types is key to mastering this pattern.
1. Static Proxy (Static Proxy)
A static proxy establishes a structural relationship determined at compile time. It requires creating a corresponding proxy class for every target object being proxied; this class must contain a reference to the target object and include logic code to invoke its methods.
- Characteristics: Simple structure, minimal performance overhead, no runtime environment support required.
- Applicable Scenarios: Suitable for scenarios where functionality is relatively fixed and there is no need to dynamically switch implementation classes.
2. Dynamic Proxy (Dynamic Proxy)
A dynamic proxy is generated at runtime based on requirements. In Java, it relies primarily on the java.lang.reflect.Proxy interface and the InvocationHandler interface provided by the JDK for implementation. It allows creating multiple different proxy instances pointing to various implementation logics of the same target object during execution.
- Characteristics: High flexibility, supports polymorphism, strong code reusability (through unified interface handling).
- Applicable Scenarios: Suitable for scenarios requiring dynamic switching of business logic, remote calls, or Aspect-Oriented Programming (AOP).
Code Practice and Limitation Analysis of Static Proxies
To intuitively demonstrate the working principle of a static proxy, we can construct a typical "Employee" system example. Assume an Employee class represents the real object, while ManagerProxy serves as its proxy class responsible for recording operation logs and controlling access permissions.
// Define target interface or abstract class
interface Employee {
void work();
}
// The actual object being proxied
class RealEmployee implements Employee {
public void work() {
System.out.println("Executing core business logic...");
}
}
// Static Proxy Class
class ManagerProxy implements Employee {
private final Employee target; // Reference to the real object
// Constructor injection of the target object, reflecting dependency relationship
public ManagerProxy(Employee employee) {
this.target = employee;
}
// Core logic: Intercept calls and enhance functionality
@Override
public void work() {
System.out.println("Recording start time: " + new java.util.Date());
target.work(); // Forward to the real object for execution
System.out.println("Operation ended, generating report");
}
}
// Test class
public class Main {
public static void main(String[] args) {
Employee real = new RealEmployee();
ManagerProxy proxy = new ManagerProxy(real);
// Client invokes through the proxy object rather than directly invoking the real object
proxy.work();
}
}
In the above example, ManagerProxy fully takes over the execution flow of the work() method. However, static proxies have obvious limitations: if multiple methods on a target class need to be intercepted (e.g., logging and permission checking simultaneously), similar proxy code must be written repeatedly for each method. This results in significant boilerplate code and difficult-to-maintain coupling issues.
Advantages of Dynamic Proxy and Application Practice with Spring AOP
To resolve the redundancy issue found in static proxies regarding functional extension, Java provides a dynamic proxy mechanism based on reflection. This approach allows us to define a unified InvocationHandler to intercept all method calls, thereby achieving uniform control over target object behavior. Furthermore, in modern enterprise development, the Aspect-Oriented Programming (AOP) framework of Spring Framework utilizes the concept of dynamic proxies to achieve elegant functional decoupling.
1. Implementation Based on Reflection
Using Proxy.newInstance() and custom handler classes, we can easily generate proxy objects for interfaces:
public class Main {
public static void main(String[] args) throws Exception {
Employee real = new RealEmployee();
// Create dynamic proxy instance
ManagerDynamicProxy proxy = (ManagerDynamicProxy)
Proxy.newInstance(ManagerDynamicProxy.class, real, new InvocationHandler() {
@Override
public Object invoke(Object obj, Method method, Object[] args) throws Throwable {
System.out.println("Recording start time: " + new java.util.Date());
return method.invoke(real, args); // Forward the call
}
});
proxy.work();
}
}
// Dynamic Proxy Class Implementation of InvocationHandler
class ManagerDynamicProxy implements InvocationHandler {
private final Object target;
public ManagerDynamicProxy(Object obj) {
this.target = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[Dynamic Proxy] Executing pre-check...");
// Permission verification or transaction initiation can be performed here
return method.invoke(target, args);
}
}
2. Automated Application of Spring AOP
In actual projects, developers typically do not manually write the complex reflection code described above. The Spring Framework automatically handles proxy object generation and aspect weaving through XML configuration or annotations (such as @Aspect, @Transactional). For instance, when adding a @Transactional annotation to a method in the Service layer, Spring will automatically create a dynamic proxy object for that method's call at runtime, injecting transaction logic before and after the method execution. This mechanism drastically reduces code complexity, completely separating business logic from cross-cutting concerns, making it one of the best practices for implementing design patterns in real-world engineering projects.
Summary and Adherence to Design Principles
Mastering the Proxy Pattern means not only being able to write the aforementioned code snippets but also understanding the underlying design philosophy: The Open/Closed Principle (OCP) and The Dependency Inversion Principle (DIP). By introducing a proxy layer, changes in concrete business logic do not affect callers, while non-core functionalities (such as logging or security validation) can be developed and tested independently of the main business process. Whether static or dynamic, their ultimate goal is to enhance system maintainability, extensibility, and security, laying a solid foundation for building high cohesion and low coupling software architectures.