Java Dynamic Proxy Guide

In Java, the proxy pattern is a commonly used design pattern that provides an object as a surrogate or placeholder for another object to control access to it. A proxy object can add extra logic before and after accessing the original object, such as permission checks, logging, performance monitoring, etc., thereby enhancing program maintainability, scalability, and security.

Proxies in Java are divided into two types: static proxies and dynamic proxies.

A static proxy is defined at compile time with a fixed relationship between the proxy object and the target (original) object. Both the proxy object and the original object must implement the same interface or inherit from the same parent class. A static proxy requires manually writing a proxy class, where the proxy functionality is implemented by calling methods of the original object within the proxy class. The disadvantage of static proxies is that they require manual creation of proxy classes; for large amounts of business logic and object types, numerous proxy classes need to be written, increasing program maintenance difficulty.

A dynamic proxy defines the relationship between the proxy object and the target object dynamically at runtime. The proxy object and the original object do not necessarily implement the same interface or inherit from the same parent class. Dynamic proxies use Java's reflection mechanism to generate proxy objects dynamically and call methods of the original object within the proxy object's methods. There are two main implementation approaches for dynamic proxies in Java: one based on JDK (Java Development Kit) and another based on third-party libraries, such as CGLIB.

The JDK-based proxy uses Java reflection mechanisms and the Dynamic Proxy API to implement functionality. It can proxy any class that implements an interface without requiring manual creation of a proxy class. A JDK-based proxy requires an implementation of the InvocationHandler interface to handle method invocations on the proxy object. Method calls on the proxy object are forwarded to the implementation of the InvocationHandler interface, thereby achieving proxy functionality. The disadvantage of JDK-based proxies is that they can only proxy classes that implement interfaces and cannot proxy classes without implemented interfaces (i.e., final classes or those lacking an explicit interface).

The CGLIB-based proxy uses bytecode generation technology to implement its functionality. It can proxy any class, including those that do not implement interfaces. A CGLIB-based proxy requires an implementation of the MethodInterceptor interface to handle method invocations on the proxy object; these calls are forwarded to the MethodInterceptor implementation to achieve proxying. The disadvantage of CGLIB-based proxies is that the generated proxy objects can be relatively heavy, and for some special classes, proxy generation may fail (e.g., if a class has no public methods or contains private constructors).

Proxies are widely used in actual development across domains such as AOP (Aspect-Oriented Programming) and RPC (Remote Procedure Call), among others. Through proxies, different business logics can be separated, reducing coupling and improving program maintainability and scalability.

Example

Here is a simple Java proxy example:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public interface Hello {
    void sayHello();
}

public class HelloImpl implements Hello {
    public void sayHello() {
        System.out.println("Hello, World!");
    }
}

public class HelloProxy implements InvocationHandler {
    private Object target;

    public HelloProxy(Object target) {
        this.target = target;
    }

    @Override // Added for clarity in English context
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before invoke method: " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("After invoke method: " + method.getName());
        return result;
    }

    public static void main(String[] args) {
        Hello hello = new HelloImpl();
        
        // Create the proxy instance using Proxy.newProxyInstance
        Hello proxy = (Hello) Proxy.newProxyInstance(
            hello.getClass().getClassLoader(),
            hello.getClass().getInterfaces(),
            new HelloProxy(hello)
        );

        proxy.sayHello();
    }
}

Run Output:

Before invoke method: sayHello
Hello, World!
After invoke method: sayHello

In this example, we define an interface Hello and its implementation class HelloImpl. Then, we create a proxy class HelloProxy, which implements the InvocationHandler interface. This interface contains only one method, invoke; when a method on the proxy object is called, the invoke method is automatically invoked. Inside the HelloProxy class, we define an instance variable target, which points to the actual object—the instance of HelloImpl. Within the invoke method, we can add our own logic before and after calling methods on the target object; for example, printing logs before and after the execution of a specific method. Finally, in the main method, we create a proxy object that intercepts method calls within the Hello interface and redirects them to the corresponding methods in the HelloImpl object. Executing this code produces the output shown above.