08 Proxy Pattern and Common Implementations

08 Proxy Pattern and Common Implementations #

The dynamic proxy mechanism is widely used in Java, for example, Spring AOP, MyBatis, Hibernate, and other commonly used open-source frameworks all use dynamic proxy mechanism. Of course, Dubbo also uses dynamic proxy, and when we develop a simplified version of the RPC framework later, we will refer to the dynamic proxy mechanism used by Dubbo to shield the underlying network transmission and service discovery implementations.

In this lesson, we will start with the basics and first introduce the basic concept of the proxy pattern. We will then focus on the usage and underlying implementation of JDK dynamic proxy, and also explain some limitations of JDK dynamic proxy. Finally, we will introduce dynamic proxy based on bytecode generation.

Proxy Pattern #

The proxy pattern is one of the 23 object-oriented design patterns. Its class diagram is shown below:

image

In the diagram, Subject is the business logic interface in the program, RealSubject is the actual business class that implements the Subject interface, and Proxy is the proxy class that implements the Subject interface and encapsulates a reference to a RealSubject. We do not directly call methods of the RealSubject object in the program, but instead use the Proxy object to achieve the relevant functionality.

The implementation of the Proxy.operation() method will invoke the operation() method of the encapsulated RealSubject object to execute the actual business logic. The role of the proxy is not only to complete the business logic as usual, but also to add some proxy logic before and after the business logic. In other words, the Proxy.operation() method will perform some pre-processing and post-processing before and after the invocation of RealSubject.operation() method. This is what we commonly refer to as the “proxy pattern”.

By using the proxy pattern, we can control the program’s access to the RealSubject object. If any abnormal access is detected, we can directly limit or return, and we can also perform relevant pre-processing and post-processing before and after the execution of the business logic to help the upper-level caller shield the underlying details. For example, in an RPC framework, the proxy can complete a series of operations such as serialization, network I/O, load balancing, fault recovery, and service discovery, while the upper-level caller only perceives a local invocation.

The proxy pattern can also be used to implement lazy loading. We know that querying the database is a time-consuming operation, and sometimes the queried data is not actually used by the program. The lazy loading feature can effectively avoid this waste. When the system accesses the database, it can first obtain a proxy object, at which point no database query operation is performed, and naturally there is no actual data in the proxy object; when the system really needs to use the data, it can then call the proxy object to perform the database query and return the data. The principle of lazy loading in popular ORM frameworks, such as MyBatis and Hibernate, is roughly the same.

In addition, the proxy object can coordinate the relationship between the actual RealSubject object and the caller, achieving a certain degree of decoupling.

JDK Dynamic Proxy #

The implementation of the proxy pattern mentioned above is also called the “static proxy pattern” because a Proxy class has to be created for each RealSubject class at compile time. When there are many classes to be proxied, a large number of Proxy classes will appear.

In this scenario, we can use JDK dynamic proxy to solve this problem. The core of JDK dynamic proxy is the InvocationHandler interface. Here is a demo implementation of InvocationHandler:

public class DemoInvokerHandler implements InvocationHandler {

    private Object target; // The actual business object, i.e., the RealSubject object

    public DemoInvokerHandler(Object target) { // Constructor
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable {
        // ... Pre-processing before executing the business method ...
        Object result = method.invoke(target, args);
        // ... Post-processing after executing the business method ...
        return result;
    }

    public Object getProxy() {
        // Create the proxy object
        return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                target.getClass().getInterfaces(), this);
    }

}

Next, we can create a main() method to simulate the upper-level caller, create and use dynamic proxy:

public class Main {

    public static void main(String[] args) {
        Subject subject = new RealSubject();
        DemoInvokerHandler invokerHandler = new DemoInvokerHandler(subject);
        Subject proxy = (Subject) invokerHandler.getProxy();
        proxy.operation();
    }

}

In the above code, we create a RealSubject object as the actual business object, create a DemoInvokerHandler object with the RealSubject object as a parameter, and then call the getProxy() method of the DemoInvokerHandler to get the proxy object. Finally, we call the operation() method of the proxy object, which will invoke the operation() method of the RealSubject object through the invoke() method of the InvocationHandler.

    private Class<?> apply(ClassLoader loader,
                           Class<?>[] interfaces) {
        synchronized (proxies) {
            // Step 1: 检查接口数量是否超出限制
            checkProxyClassLimit(interfaces.length);

            // Step 2: 构造代理类的名称
            String proxyPkg = null; // 包名,略
            String proxyName = proxyPkg + proxyClassNamePrefix +
                    nextUniqueNumber.getAndIncrement();
            // 创建代理类的完整类名,例如:com.sun.proxy.$Proxy1
            String proxyClassName = proxyName;
 
            // Step 3: 写入代理类的字节码文件
            Class<?> cl = null;
            try {
                // 生成代理类的字节码内容
                byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                        proxyClassName, interfaces, accessFlags);
                // 写入字节码文件
                cl = defineClass0(loader, proxyClassName,
                        proxyClassFile, 0, proxyClassFile.length);
            } catch (IOException e) {
                // 异常处理,略
            }

            // Step 4: 加载代理类
            resolveClass(cl);
            return cl;
        }
    }
public Class apply(ClassLoader loader, Class[] interfaces) {

    // ... A series of checks on the `interfaces` collection

    // ... Select the package name of the proxy class (omitted)

    // The name of the proxy class consists of the package name, the prefix of the proxy class name, and the number

    long num = nextUniqueNumber.getAndIncrement();

    String proxyName = proxyPkg + proxyClassNamePrefix + num;

    // Generate the proxy class and write it to a file

    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(

            proxyName, interfaces, accessFlags);

    

    // Load the proxy class and return the Class object

    return defineClass0(loader, proxyName, proxyClassFile, 0, 

      proxyClassFile.length);

}

The ProxyGenerator.generateProxyClass() method generates the bytecode of the proxy class based on the specified name and interface collection, and decides whether to save it to the disk based on certain conditions. The code of this method is as follows:

public static byte[] generateProxyClass(final String name,

       Class[] interfaces) {

    ProxyGenerator gen = new ProxyGenerator(name, interfaces);

    // Generate the bytecode of the proxy class dynamically. The specific generation process is not discussed in detail here. Interested readers can continue to analyze.

    final byte[] classFile = gen.generateClassFile();

    // If `saveGeneratedFiles` is `true`, the bytecode of the generated proxy class will be saved to a file.

    if (saveGeneratedFiles) { 

        java.security.AccessController.doPrivileged(

            new java.security.PrivilegedAction() {

                public Void run() {

                    // Omitted try/catch block

                    FileOutputStream file = new FileOutputStream(

                        dotToSlash(name) + ".class");

                    file.write(classFile);

                    file.close();

                    return null;

                }

            }

        );

    }

    return classFile; //Return the bytecode of the generated proxy class

}

Finally, to see the actual definition of the dynamically generated proxy class by the JDK, we need to decompile the generated bytecode of the proxy class. The generated proxy class for RealSubject in the example above is decompiled into the following code:

public final class $Proxy37 

      extends Proxy implements Subject {  // Implementing the `Subject` interface

    // Related methods and properties inherited from the `Object` class are omitted here

    private static Method m3;

    static {

        // Omitted try/catch block

        // Record the `Method` object corresponding to the `operation()` method

        m3 = Class.forName("com.xxx.Subject")

          .getMethod("operation", new Class[0]);

    }

    // The parameter of the constructor is the `DemoInvokerHandler` object we used in the example

    public $Proxy11(InvocationHandler var1) throws {

        super(var1); 

    }

    public final void operation() throws {

        // Omitted try/catch block

        // Call the `invoke()` method of the `DemoInvokerHandler` object and ultimately call the corresponding method of the `RealSubject` object

        super.h.invoke(this, m3, (Object[]) null);

    }

}

To summarize, the basic usage and core principles of JDK dynamic proxy have been introduced. In summary, the implementation principle of JDK dynamic proxy is to dynamically create a proxy class and load it by the specified class loader. When creating a proxy object, the InvocationHandler object is passed as a parameter to the constructor. When calling the proxy object, the InvocationHandler.invoke() method is called, which executes the proxy logic and ultimately calls the corresponding method of the actual business object.

CGLib #

JDK dynamic proxy is natively supported by Java and does not require any external dependencies. However, as analyzed above, it can only be used for proxying based on interfaces, and it is not applicable for classes that do not implement any interfaces.

To proxy classes that do not implement any interfaces, CGLib can be used.

CGLib (Code Generation Library) is a bytecode generation library based on ASM. It allows us to modify and generate bytecode at runtime. CGLib uses bytecode technology to implement dynamic proxy functionality. The underlying principle is to generate a subclass for the target class through bytecode technology, and in this subclass, intercept all method calls of the parent class using method interception, thus achieving the purpose of proxy.

Since CGLib uses the approach of generating subclasses to implement dynamic proxy, it cannot proxy methods that are marked as final (as final methods cannot be overridden). Therefore, CGLib and JDK dynamic proxy can complement each other: when the target class implements interfaces, JDK dynamic proxy can be used to create the proxy object; when the target class does not implement any interfaces, CGLib can be used to implement the dynamic proxy functionality. In many open-source frameworks such as Spring and MyBatis, combined usage of JDK dynamic proxy and CGLib can be seen.

CGLib has two important components in its implementation.

  • Enhancer: It specifies the target object to be proxied and the object that actually handles the proxy logic. The proxy object is obtained by invoking the create() method, and calls to all non-final methods of this object will be forwarded to the MethodInterceptor for processing.
  • MethodInterceptor: All method calls to the dynamic proxy object are intercepted and processed in the intercept method.

The usage of these two components is similar to that of Proxy and InvocationHandler in JDK dynamic proxy.

Let’s explore the usage of CGLib through an example. When creating a dynamic proxy class with CGLib, we need to define an implementation of the Callback interface. CGLib also provides several sub-interfaces of the Callback interface, as shown in the following diagram:

image

Taking the MethodInterceptor interface as an example, first we need to include the Maven dependency for CGLib:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

Here is the specific code for the CglibProxy class, which implements the MethodInterceptor interface:

public class CglibProxy implements MethodInterceptor {

    // Initialize the Enhancer object

    private Enhancer enhancer = new Enhancer(); 

    

    public Object getProxy(Class clazz) {

        enhancer.setSuperclass(clazz); // Specify the superclass of the generated proxy class

        enhancer.setCallback(this); // Set the Callback object

        return enhancer.create(); // Dynamically create a subclass instance through ASM bytecode technology

    }

    

    // Implement the `intercept()` method of the MethodInterceptor interface

    public Object intercept(Object obj, Method method, Object[] args,

                   MethodProxy proxy) throws Throwable {

        System.out.println("Preprocessing");

        Object result = proxy.invokeSuper(obj, args); // Call the method in the parent class

        System.out.println("Postprocessing");

        return result;

    }

}  

Next, we write another target class to be proxied and a main method for testing, as follows:

public class CGLibTest { // Target class

    public String method(String str) {   // Target method

        System.out.println(str);

        return "CGLibTest.method():" + str;

    }

    

    public static void main(String[] args) {

        CglibProxy proxy = new CglibProxy();

        // Generate a proxy object of CBLibTest

        CGLibTest proxyImp = (CGLibTest) 

            proxy.getProxy(CGLibTest.class);

        // Call the `method()` method of the proxy object

        String result = proxyImp.method("test");

        System.out.println(result);

        // ----------------

        // The output is as follows:

        // Preprocessing

        // test

        // Postprocessing

        // CGLibTest.method():test

    }

}

That concludes the introduction to the basics of using CGLib. We will continue to introduce related CGLib content when discussing Dubbo source code in the future.

Javassist #

Javassist is an open-source library for generating Java bytecodes. Its main advantages are simplicity and speed. It allows dynamic modification of class structures or dynamic generation of classes by directly using the Java APIs provided by Javassist.

Using Javassist is relatively simple. First, let’s see how to dynamically create classes using the Java API provided by Javassist. The example code is as follows:

public class JavassistMain {

    public static void main(String[] args) throws Exception {

        ClassPool cp = ClassPool.getDefault(); // Create a ClassPool

        // The name of the class to be generated is com.test.JavassistDemo

        CtClass clazz = cp.makeClass("com.test.JavassistDemo");

    

        StringBuffer body = null;

        // Create a field, specifying the field type, field name, and the class to which the field belongs

        CtField field = new CtField(cp.get("java.lang.String"), 

            "prop", clazz);

        // Specify private modifier for this field

        field.setModifiers(Modifier.PRIVATE);

    

        // Set the getter/setter methods of the `prop` field

        clazz.addMethod(CtNewMethod.setter("getProp", field));

        clazz.addMethod(CtNewMethod.getter("setProp", field));

        // Set the initial value of the `prop` field and add the field to clazz

        clazz.addField(field, CtField.Initializer.constant("MyName"));

    

        // Create a constructor, specifying the parameter types and the class to which the constructor belongs

        CtConstructor ctConstructor = new CtConstructor(

            new CtClass[]{}, clazz);

        // Set the method body

        body = new StringBuffer();

        body.append("{\n prop=\"MyName\";\n}");

        ctConstructor.setBody(body.toString());

        clazz.addConstructor(ctConstructor); // Add the constructor to clazz

    

        // Create the `execute()` method, specifying the return type, method name, method parameter list, and

        // the class to which the method belongs

        CtMethod ctMethod = new CtMethod(CtClass.voidType, "execute",

                new CtClass[]{}, clazz);

        // Specify the public modifier for this method

        ctMethod.setModifiers(Modifier.PUBLIC);

        // Set the method body

        body = new StringBuffer();

        body.append("{\n System.out.println(\"execute():\" " +

                "+ this.prop);");

        body.append("\n}");

        ctMethod.setBody(body.toString());

        clazz.addMethod(ctMethod); // Add the `execute()` method to clazz

        // Save the JavassistDemo class defined above to the specified directory

        clazz.writeFile("/Users/xxx/"); 

        // Load the clazz class and create an object

        Class<?> c = clazz.toClass();

        Object o = c.newInstance();

        // Call the execute() method

        Method method = o.getClass().getMethod("execute", 

          new Class[]{});

        method.invoke(o, new Object[]{});

    }

}

After executing the above code, you can find the generated JavassistDemo.class file in the specified directory. Decompile it to get the code for JavassistDemo as follows:

public class JavassistDemo {

    private String prop = "MyName";

    public JavassistDemo() {

        prop = "MyName";

    }

    public void setProp(String paramString) {

        this.prop = paramString;

    }

    public String getProp() {

        return this.prop;

    }

    public void execute() {

        System.out.println("execute():" + this.prop);

    }

}

Javassist can also be used to implement dynamic proxy functionality. The underlying principle is also based on creating a subclass of the target class. Here, we use Javassist to create a proxy object for the generated JavassitDemo. The specific implementation is as follows:

public class JavassitMain2 {

    public static void main(String[] args) throws Exception {

        ProxyFactory factory = new ProxyFactory();

        // Specify the superclass. ProxyFactory will dynamically generate a subclass that inherits from this superclass

        factory.setSuperclass(JavassistDemo.class);

        // Set the filter to determine which method calls need to be intercepted

        factory.setFilter(new MethodFilter() {

            public boolean isHandled(Method m) {

                if (m.getName().equals("execute")) {

                    return true;

                }

                return false;

            }

        });

        // Set the interception handling

        factory.setHandler(new MethodHandler() {

            @Override

            public Object invoke(Object self, Method thisMethod, 

                Method proceed, Object[] args) throws Throwable {

                System.out.println("Preprocessing");

                Object result = proceed.invoke(self, args);

                System.out.println("Execution result:" + result);

                System.out.println("Postprocessing");

                return result;

            }

        });

        // Create a proxy class for JavassistDemo and create a proxy object

        Class<?> c = factory.createClass();

        JavassistDemo JavassistDemo = (JavassistDemo) c.newInstance();

        JavassistDemo.execute(); // The execute() method will be intercepted

        System.out.println(JavassistDemo.getProp());

    }

}

That concludes the introduction to the basics of Javassist. Javassist allows the generation of classes directly from Java language strings, making it quite useful. Javassist also has good performance and is the default proxy generation method in Dubbo.

Summary #

In this lesson, we first introduced the core concepts and uses of the proxy pattern, giving you a preliminary understanding of the proxy pattern. Then we introduced the use of JDK dynamic proxy and deeply analyzed the implementation principle of JDK dynamic proxy and its limitations. Finally, we introduced the basic usage of CGLib and Javassist, two code generation tools, and briefly described the principles of generating proxies using these two tools.

Do you know any other ways to implement dynamic proxy? Feel free to discuss in the comments.