Java code audit - 3. Dynamic Proxies

reference resources:

https://mp.weixin.qq.com/s/HtLjYHLAQQz83aoOI7D0ig

https://zhishihezi.net/b/5d644b6f81cbc9e40460fe7eea3c7925

brief introduction

Concept of agent

Here, agent is a design idea, which refers to encapsulating the module and adding additional functions to it. That is, create a proxy object for packaging, and replace the original object with the proxy object. In subsequent operations, any call to the original object must pass through the proxy object first.

It is equivalent to enhancing the function of the original module without modifying the code of the original module.

Main usage scenarios of agent:

  1. Count the time taken to execute the method.
  2. Add logs before and after method execution.
  3. The parameter or return value of the detection method.
  4. Method access control.

There are two types of proxy implementation, static proxy and dynamic proxy. Dynamic agent is an improvement of static agent. It can use only one class, one method, and can serve multiple methods of multiple classes.

Suppose there are two classes, students and cars. You want to add the execution logs of all methods without modifying the original code.

  • If the static proxy implementation based on inheritance is adopted. It is to create two new subclasses, inherit and override each method of its parent class respectively, and add log output function for it. It is equivalent to overriding each method of each class.
  • Dynamic agent is adopted. Then you just need to create a proxy class and a method. You can use this method to serve multiple methods of multiple classes.

Dynamic agent is commonly used in practice, and there are many implementations of dynamic agent. Here we only focus on the dynamic agent provided by jdk.

Dynamic agent based on JDK

Related interfaces and classes

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

import java.lang.reflect.InvocationHandler;

public class Proxy implements java.io.Serializable {

    // Create proxy object
    public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
    //Classloader: just execute the class loader of the target class object, which is used to load the code of the target class and its interface
    //Class<?> [] interfaces: an array of class objects that specify all interfaces of the target class object. It is usually obtained by calling getInterfaces() using the class object of the target class
	//InvocationHandler h: this parameter type is an interface, mainly focusing on the only method in it, the invoke method. It is executed when the method is called by the proxy object. That is to say, calling any method in the proxy class object will execute to the invoke() method. Therefore, in this method, the enhanced or extended code logic is completed

	
	// Get dynamic proxy class
    public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) 
        
    
    // Returns the call handler for the proxy object binding
    public static InvocationHandler getInvocationHandler(Object proxy)
    
    //Detect whether a class is a dynamic proxy class
    public static boolean isProxyClass(Class<?> cl);

    /**
     * Defines a class object to the specified class loader
     *
     * @param loader Class loader
     * @param name   Class name
     * @param b      Class bytecode
     * @param off    Intercept start position
     * @param len    Intercept length
     * @return JVM Class object created
     */
    private static native Class defineClass0(ClassLoader loader, String name, byte[] b, int off, int len);

}
package java.lang.reflect;

import java.lang.reflect.Method;

/**
 * Each proxy instance has an associated invocation handler. When a method is called on a proxy instance, the method call is encoded and
 * Assign it to the invoke method of its call handler.
 */
public interface InvocationHandler {

    // Process method calls on proxy instances and return results. When a method is called on a proxy instance associated with a method, it is called in the call handler.
	// Proxy: it is a reference of proxy class object, that is, the return value of Proxy.newProxyInstrance. This reference is rarely used.
    // Method: the method object of the corresponding method that triggers invoke execution.
    // args is an array of objects containing the parameter values of the method call on the incoming proxy instance. It is null if the interface method does not use parameters. The parameters of the base type are wrapped in an instance of the appropriate base wrapper class, such as java.lang.Integer or java.lang.Boolean.
	// Returns the value returned from the method call of the proxy instance.
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

Code example

Interface

import java.io.Serializable;
public interface SayHelloInterface extends Serializable {
    public String sayHello(String name);
}

Interface implementation class

public class SayHelloImpl implements SayHelloInterface {

    @Override
    public String sayHello(String name) {
        String ret = "this is SayHelloImpl.sayHello method! Let`s say " + name;
        System.out.println(ret);
        return ret;
    }
}

Proxy object class

package Proxy;

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

public class SayHelloInvocation implements InvocationHandler {
    private Object target;

    public SayHelloInvocation() {

    }

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

    public Object bind(Object object) {
        this.target = object;
        return Proxy.newProxyInstance(
                object.getClass().getClassLoader(),
                object.getClass().getInterfaces(),
                this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // Proxy only the sayHello method
        if (!("sayHello".equals(method.getName()))) {
            return method.invoke(target, args);
        }

        if(Proxy.getInvocationHandler(proxy) == this){
            System.out.println("proxy == this");
        }
        
        System.out.println("About to call[ " + target.getClass().getName() + " ]Class[ " + method.getName() + " ]method...");
        Object obj = method.invoke(target, args);
        System.out.println("Completed[ " + target.getClass().getName() + " ]Class[ " + method.getName() + " ]Method call...");

        return obj;
    }
}

The dynamic proxy class is called in the code.

package Proxy;

import org.junit.Test;
import java.lang.reflect.Proxy;

public class TestProxy {
    @Test
    public void test1(){
        {
            SayHelloInterface sayHelloProxyInstance = (SayHelloInterface) Proxy.newProxyInstance(
                    SayHelloImpl.class.getClassLoader(),    // Specifies the class loader for the dynamic proxy class
                    SayHelloImpl.class.getInterfaces(), // new Class[]{SayHelloInterface.class}, / / defines the interface implemented by the class generated by the dynamic proxy
                    new SayHelloInvocation(new SayHelloImpl())   // Dynamic proxy processing class
            );
            sayHelloProxyInstance.sayHello("liuyun");
        }

//        {
//            SayHelloInvocation sayHelloInvocation = new SayHelloInvocation();
//            SayHelloInterface sayHelloProxyInstance = (SayHelloInterface) sayHelloInvocation.bind(new SayHelloImpl());
//            sayHelloProxyInstance.sayHello("liuyun");
//        }

//        Object string = ((SayHelloInterface) ((new SayHelloInvocation()).bind(new SayHelloImpl()))).sayHello("liuyun");
    }

}

It is worth noting that:

SayHelloInvocation and SayHelloImpl are juxtaposed. Since it cannot be converted to each other, it is necessary to use the common interface of both parties to receive.

Dynamic proxy actually generates an object in memory, which implements the interface owned by the specified target class object. Therefore, the proxy class object and the target class object are juxtaposed. They cannot be converted to each other. When using the Spring framework later, if you configure the dynamic proxy of JDK, you must use the interface type to receive the proxy class.

Two methods of creating proxy class instances

We can use the Proxy.newProxyInstance method to directly create a dynamic proxy class instance, or we can use Proxy.getProxyClass() to obtain the proxy class object and create it by reflection.

Let's take the com.anbai.sec.proxy.FileSystem interface as an example to demonstrate how to create its dynamic proxy class instance.

// Create an instance of the UnixFileSystem class
FileSystem fileSystem = new UnixFileSystem();

// Using JDK dynamic proxy to generate FileSystem dynamic proxy class instances
FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance(
      FileSystem.class.getClassLoader(),// Specifies the class loader for the dynamic proxy class
      new Class[]{FileSystem.class}, // Defines the interface implemented by the class generated by the dynamic proxy
      new JDKInvocationHandler(fileSystem)// Dynamic proxy processing class
);
// Create an instance of the UnixFileSystem class
FileSystem fileSystem = new UnixFileSystem();

// Create a dynamic proxy processing class
InvocationHandler handler = new JDKInvocationHandler(fileSystem);

// Generate a dynamic proxy class by specifying the class loader and the interface array implemented by the class
Class proxyClass = Proxy.getProxyClass(
      FileSystem.class.getClassLoader(),// Specifies the class loader for the dynamic proxy class
      new Class[]{FileSystem.class}// Defines the interface implemented by the class generated by the dynamic proxy
);

// Use reflection to get the Proxy class constructor and create a dynamic Proxy class instance
FileSystem proxyInstance = (FileSystem) proxyClass.getConstructor(
      new Class[]{InvocationHandler.class}).newInstance(new Object[]{handler}
);

Generate $ProxyXXX class code analysis

A technique for generating proxy objects in memory. The whole proxy process is carried out in memory. There is no need to write the code of the proxy class manually, nor is there a compilation process of the proxy class. Instead, a proxy class object is generated in the JVM directly during the Java runtime for our use.

Dynamic proxy actually generates an object in memory, which implements the interface owned by the specified target class object. Therefore, the proxy class object and the target class object are juxtaposed.

The java.lang.reflect.Proxy class implements the non intrusive class method proxy function by creating a new Java class (the class name is com.sun.proxy.$ProxyXXX).

The classes generated by dynamic proxy have the following technical details and features:

  1. The dynamic proxy must be an interface class. An interface implementation class is generated dynamically to proxy the method call of the interface (reflection mechanism).
  2. The dynamic proxy class is created by java.lang.reflect.Proxy.ProxyClassFactory.
  3. ProxyClassFactory will call the sun.misc.ProxyGenerator class to generate the bytecode of this class, and call the java.lang.reflect.Proxy.defineClass0() method to register this class with the JVM.
  4. This class inherits from java.lang.reflect.Proxy and implements the interface class to be proxied. Because java.lang.reflect.Proxy class implements the java.io.Serializable interface, the proxied class supports serialization and deserialization.
  5. This class implements the proxy interface class (the interface class in the example is com.anbai.sec.proxy.FileSystem), which will dynamically generate all methods of the interface class (FileSystem) through the ProxyGenerator,
  6. Because this class implements the interface class of the agent, the current class is an instance of the interface class of the agent (proxyinstance instanceof file system is true), but not an instance of the implementation class of the agent interface class (proxyinstance instanceof unixfile system is false).
  7. This class of methods contains all the methods of the interface class being proxied. The method execution results are obtained by calling the invoke method of the dynamic proxy processing class (InvocationHandler).
  8. This class overrides the toString, hashCode and equals methods of the java.lang.Object class by proxy.
  9. If multiple dynamic proxy classes are generated by moving the dynamic proxy, the 0 in the newly generated class name will increase automatically, such as com.sun.proxy.$Proxy0/$Proxy1/$Proxy2.

com.sun.proxy.$Proxy0 class code generated by dynamic proxy:

copypackage com.sun.proxy.$Proxy0;

import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements FileSystem {

    private static Method m1;

  // Implement the file system interface method. If there are multiple methods in the file system, n member variables will start from m3 in this class
    private static Method m3;

    private static Method m0;

    private static Method m2;

    public $Proxy0(InvocationHandler var1) {
        super(var1);
    }

    public final boolean equals(Object var1) {
        try {
            return (Boolean) super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String[] list(File var1) {
        try {
            return (String[]) super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final int hashCode() {
        try {
            return (Integer) super.h.invoke(this, m0, (Object[]) null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() {
        try {
            return (String) super.h.invoke(this, m2, (Object[]) null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.anbai.sec.proxy.FileSystem").getMethod("list", Class.forName("java.io.File"));
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

Serialization problem

The dynamic proxy class meets the Java object serialization conditions and will be specially handled by ObjectInputStream/ObjectOutputStream during serialization / deserialization.

FileSystemProxySerializationTest sample code:

package com.anbai.sec.proxy;

import java.io.*;
import java.lang.reflect.Proxy;

/**
 * Creator: yz
 * Date: 2020/1/14
 */
public class FileSystemProxySerializationTest {

   public static void main(String[] args) {
      try {
         // Create an instance of the UnixFileSystem class
         FileSystem fileSystem = new UnixFileSystem();

         // Using JDK dynamic proxy to generate FileSystem dynamic proxy class instances
         FileSystem proxyInstance = (FileSystem) Proxy.newProxyInstance(
               FileSystem.class.getClassLoader(),// Specifies the class loader for the dynamic proxy class
               new Class[]{FileSystem.class}, // Defines the interface implemented by the class generated by the dynamic proxy
               new JDKInvocationHandler(fileSystem)// Dynamic proxy processing class
         );

         ByteArrayOutputStream baos = new ByteArrayOutputStream();

         // Create Java object serialization output stream object
         ObjectOutputStream out = new ObjectOutputStream(baos);

         // Serialize dynamic proxy class
         out.writeObject(proxyInstance);
         out.flush();
         out.close();

         // Create a binary input stream object using the binary array generated by the dynamic proxy class for deserialization
         ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());

         // Create a Java object input stream object by deserializing the input stream (bais)
         ObjectInputStream in = new ObjectInputStream(bais);

         // Deserialize the input stream data as a FileSystem object
         FileSystem test = (FileSystem) in.readObject();

         System.out.println("Deserialize class instance class name:" + test.getClass());
         System.out.println("Deserialize class instance toString:" + test.toString());
      } catch (IOException e) {
         e.printStackTrace();
      } catch (ClassNotFoundException e) {
         e.printStackTrace();
      }

   }

}

Program execution results:

Deserialize class instance class name:class com.sun.proxy.$Proxy0
 Deserialize class instance toString:com.anbai.sec.proxy.UnixFileSystem@b07848

The Class generated by the dynamic proxy will not serialize the member variables of the Class during deserialization / deserialization, and the serialVersionUID is 0L, which means that when the Class object of the Class is passed to the static lookup method of java.io.ObjectStreamClass, the returned ObjectStreamClass instance will have the following characteristics:

  1. Calling its getSerialVersionUID method will return 0L.
  2. Calling its getFields method returns an array of zero length.
  3. Calling its getField method will return null.

However, its parent class (java.lang.reflect.Proxy) is not affected during serialization. The H variable (InvocationHandler) in the parent class will be serialized. This h stores the processing class instance of the dynamic proxy class and the implementation class instance of the interface class of the dynamic proxy.

When serializing objects generated by dynamic proxy (com.sun.proxy.$ProxyXXX), a special protocol will be used: TC_PROXYCLASSDESC(0x7D), which is defined in java.io.ObjectStreamConstants. During deserialization, the resolveClass method of the java.io.ObjectInputStream class will not be called, but the resolveProxyClass method will be called to convert it into a class object.

For detailed description, please refer to: Dynamic Proxy Classes-Serialization

Posted on Sat, 04 Dec 2021 23:24:31 -0500 by sameerni