JDK source code analysis of java Dynamic agent

Keywords: Java JDK Eclipse

According to the Convention on the Internet, first give an example of static agent to dynamic agent; otherwise, the following theory and source code analysis are not well prepared, and everyone is a bit confused.

Static agent:

/**
 * Voice interface for proxy interface
 */
public interface IVoice {

    void song();
}

 

/**
 * Singers
 */
public class Singer implements IVoice{

    public void song(){
        System.out.println("The singer is singing...");
    }
}

 

/**
 * Agent class: speaker (English words are too difficult, I'm afraid you don't know, just use Pinyin)
 */
public class LaBa implements IVoice{

    private IVoice voice;

    public LaBa(IVoice voice) {
        this.voice = voice;
    }

    public void song() {
        System.out.println("I amplified the singer's voice");
        this.voice.song();
        System.out.println("The singer stopped talking");
    }
}

Execution class:

public class Main {
    public static void main(String[] args) {
        Singer singer=new Singer();
        IVoice voice=new LaBa(singer);
        voice.song();
    }
}

Print results:

 

The above interface doesn't seem to work. What if we remove it?

/**
 * Singers
 */
public class Singer {
    public void song(){
        System.out.println("The singer is singing...");
    }
}

 

/**
 * Agent class: speaker (English words are too difficult, I'm afraid you don't know, just use Pinyin)
 */
public class LaBa {
    private Singer singer;

    public LaBa(Singer singer) {
        this.singer = singer;
    }
    public void  song(){
        System.out.println("I amplified the singer's voice");
        this.singer.song();
        System.out.println("The singer stopped talking");
    }
}

 

public class Main {
    public static void main(String[] args) {
        Singer singer=new Singer();
        LaBa laba=new LaBa(singer);
        laba.song();
    }
}

The printing results are as like as two peas. The agent without interface is not an agent yet.

Or I can say that the name of the proxy method song() is not the same as the name of the song() method in the Singer class.

This way:

/**
 * Singers
 */
public class Singer {
    public void song(){
        System.out.println("The singer is singing...");
    }
}

/**
 * Agent class: speaker (English words are too difficult, I'm afraid you don't know, just use Pinyin)
 */
public class LaBa {
    private Singer singer;

    public LaBa(Singer singer) {
        this.singer = singer;
    }
    public void  labaSong(){
        System.out.println("I amplified the singer's voice");
        this.singer.song();
        System.out.println("The singer stopped talking");
    }
}


public class Main {
    public static void main(String[] args) {
        Singer singer=new Singer();
        LaBa laba=new LaBa(singer);
        laba.labaSong();
    }
}

There is no interface, and the methods of proxy class and actual execution class are different. Is proxy still considered?

Find Du Niang

 

There is no stipulation in the definition that the proxy can only be called by using the same interface as the proxy method name. So as long as we realize the access of one object to another and the method call is the proxy mode, now it seems that we can call any object method casually in the code is the use of one mode.

Let's go back to the static agents with interfaces that are recognized by everyone;

As you can see from the static agent example,

We actually created a proxy class LaBa.java, and the editor was compiled into a class bytecode file, which was imported by the virtual machine for verification and analysis

Finally, an object is created in memory, which enables it to implement the proxy function for us to use;

This mode can meet the needs when determined by the agent's method; otherwise, the static agent mode cannot be used.

Compared with static agent, it is dynamic agent.

Dynamic agent pattern is a pattern that uses programs to create agent objects.

JDK dynamic agent mode:

According to the Convention, code first, and be familiar with the basic usage.

/**
 * Voice interface for proxy interface
 */
public interface IVoice {
    Object song();
}
/**
 * Singers
 */
public class Singer implements IVoice{
    public Object song(){
        System.out.println("The singer is singing...");
        return null;
    }
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

//Method delegation class
public class MethodHandler implements InvocationHandler {

    //Original object
    private Object orgObject;

    public MethodHandler(Object orgObject) {
        this.orgObject = orgObject;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("I amplified the singer's voice");
        //res Is the return value of the proxied method
        Object res=method.invoke(orgObject,args);
        System.out.println("The singer stopped talking");
        return res;
    }
}
import java.lang.reflect.Proxy;

//Execution class
public class Main {
    public static void main(String[] args) {
        Singer singer=new Singer();
        MethodHandler methodHandler=new MethodHandler(singer);
        IVoice proxy=(IVoice) Proxy.newProxyInstance(singer.getClass().getClassLoader(), singer.getClass().getInterfaces(), methodHandler);
        proxy.song();
    }
}

Print results:

 

We usually develop java projects on the idea or eclipse. The editor helps us to compile or compile them into class files, which can be executed directly;

That is to say, the editor helps us to do a good job of javac (compile into class file) and java (execute java program) commands.

So the jdk agent is

First, create a class, then compile it into a class file, load it into a virtual machine, and create a class object

Or create a class file directly, load it into the virtual machine, and create a class object,

Or we can use black technology to directly skip the detection and analysis of virtual machine and create an object in the virtual machine.

 

We need to start with the source code.

First, let's look at the method Proxy.newProxyInstance()

@CallerSensitive
    public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * Invoke its constructor with the designated invocation handler.
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

Remove redundant code and add some comments

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) throws IllegalArgumentException{      

        final Class<?>[] intfs = interfaces.clone();     
        //Get proxy class object
        Class<?> cl = getProxyClass0(loader, intfs);  
   
         //Constructor function of proxy class object
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            //Instantiate an object
            return cons.newInstance(new Object[]{h});
        } 
    }                

Continue to see the getProxyClass0() method

private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }

        // If the proxy class defined by the given loader implementing
        // the given interfaces exists, this will simply return the cached copy;
        // otherwise, it will create the proxy class via the ProxyClassFactory
        //If there is something in the cache, it can be accessed directly. If not, it can be passed ProxyClassFactory Class creation
        return proxyClassCache.get(loader, interfaces);
    }    

 

Here, if you take it out of the Cache directly, there must be a way to add it if you have get. Look at the comments above. At present, we don't know where the ProxClassFactory class is or how to create it.

Or go to proxyClassCache.get(loader, interfaces); methods;

 1 public V get(K key, P parameter) {
 2         Objects.requireNonNull(parameter);
 3 
 4         expungeStaleEntries();
 5 
 6         Object cacheKey = CacheKey.valueOf(key, refQueue);
 7 
 8         // lazily install the 2nd level valuesMap for the particular cacheKey
 9         ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
10         if (valuesMap == null) {
11             ConcurrentMap<Object, Supplier<V>> oldValuesMap
12                 = map.putIfAbsent(cacheKey,
13                                   valuesMap = new ConcurrentHashMap<>());
14             if (oldValuesMap != null) {
15                 valuesMap = oldValuesMap;
16             }
17         }
18 
19         // create subKey and retrieve the possible Supplier<V> stored by that
20         // subKey from valuesMap
21         Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
22         Supplier<V> supplier = valuesMap.get(subKey);
23         Factory factory = null;
24 
25         while (true) {
26             if (supplier != null) {
27                 // supplier might be a Factory or a CacheValue<V> instance
28                 V value = supplier.get();
29                 if (value != null) {
30                     return value;
31                 }
32             }
33             // else no supplier in cache
34             // or a supplier that returned null (could be a cleared CacheValue
35             // or a Factory that wasn't successful in installing the CacheValue)
36 
37             // lazily construct a Factory
38             if (factory == null) {
39                 factory = new Factory(key, parameter, subKey, valuesMap);
40             }
41 
42             if (supplier == null) {
43                 supplier = valuesMap.putIfAbsent(subKey, factory);
44                 if (supplier == null) {
45                     // successfully installed Factory
46                     supplier = factory;
47                 }
48                 // else retry with winning supplier
49             } else {
50                 if (valuesMap.replace(subKey, supplier, factory)) {
51                     // successfully replaced
52                     // cleared CacheEntry / unsuccessful Factory
53                     // with our Factory
54                     supplier = factory;
55                 } else {
56                     // retry with current supplier
57                     supplier = valuesMap.get(subKey);
58                 }
59             }
60         }
61     }

The code is relatively long. First, start from the return line of 30 lines and analyze upward to remove the redundant code. The code is as follows:

 1 public V get(K key, P parameter) {        
 2         Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
 3         Supplier<V> supplier = valuesMap.get(subKey);
 4         Factory factory = null;
 5 
 6         while (true) {
 7             if (supplier != null) {               
 8                 V value = supplier.get();
 9                 if (value != null) {
10                     return value;
11                 }
12             }        
13         }
14     }

Value comes from supplier; supplier comes from valuesMap.get(subKey); then it can be inferred that subKey is the key value of the proxy class object,

Then subKeyFactory.apply(key, parameter) must be the method to create the proxy class object and added to the cache.

Check the source code of subKeyFactory.apply(key, parameter):

   

Found the ProxyClassFactory class mentioned earlier. No doubt, it is the proxy class object created here

apply () source code

The source code has been deleted and Chinese Notes have been added.

 1 public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
 2             Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
 3             //We only have one interface at present, so we don't need to think about circulation
 4             for (Class<?> intf : interfaces) {
 5               
 6                 Class<?> interfaceClass = null;
 7                 try {
 8                     //Get interface class object
 9                     interfaceClass = Class.forName(intf.getName(), false, loader);
10                 }            
11                 /*
12                  * Verify that the Class object actually represents an
13                  * interface.
14                      Judge whether the passed class object parameter is an interface
15                  */
16                 if (!interfaceClass.isInterface()) {
17                     throw new IllegalArgumentException(
18                         interfaceClass.getName() + " is not an interface");
19                 }               
20             }
21 
22             String proxyPkg = null;     // package to define proxy class in
23             int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
24 
25            
26 
27             if (proxyPkg == null) {
28                 // if no non-public proxy interfaces, use com.sun.proxy package
29                 //If empty, use com.sun.proxy package Package name as proxy object
30                 proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
31             }
32 
33             /*
34              * Choose a name for the proxy class to generate.
35                nextUniqueNumber It is an AtomicLong value. It is a thread safe atomic operation. First, get the current value and add the internal long value + 1
36              */
37             long num = nextUniqueNumber.getAndIncrement();
38             // private static final String proxyClassNamePrefix = "$Proxy";
39             //The final path is like this: com.sun.proxy package.$Proxy0
40             String proxyName = proxyPkg + proxyClassNamePrefix + num;
41 
42             /*
43              * Generate the specified proxy class.
44                  Create a specified proxy class
45              */
46             byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
47                 proxyName, interfaces, accessFlags);
48             try {
49                 return defineClass0(loader, proxyName,
50                                     proxyClassFile, 0, proxyClassFile.length);
51             }
52         }
53     }

 

It can be inferred that 46 lines generate byte array. defineClass0() instantiates byte array into an object and returns it to the superior method, and then returns it to us. Here, first put defineClass0() on the table, and then look at it

How ProxyGenerator.generateProxyClass() generates byte arrays.

Go in and have a look.

 1 public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) {
 2         ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);        
 3         final byte[] var4 = var3.generateClassFile();
 4         //private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));
 5         //Save the resulting file
 6         if (saveGeneratedFiles) {
 7             AccessController.doPrivileged(new PrivilegedAction<Void>() {
 8                 public Void run() {
 9                     try {
10                         int var1 = var0.lastIndexOf(46);
11                         Path var2;
12                         if (var1 > 0) {
13                             Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar));
14                             Files.createDirectories(var3);
15                             var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
16                         } else {
17                             var2 = Paths.get(var0 + ".class");
18                         }
19 
20                         Files.write(var2, var4, new OpenOption[0]);
21                         return null;
22                     } catch (IOException var4x) {
23                         throw new InternalError("I/O exception saving generated file: " + var4x);
24                     }
25                 }
26             });
27         }
28 
29         return var4;
30     }

return from line 27 to analyze; save generated files from line 6 to determine whether to save the generated files. It can be inferred that var4 is the final complete byte array.

This place remembers the path sun.misc.ProxyGenerator.saveGeneratedFiles in line 4. Later, we need to use this path to output the bytecode file to the local.

Since it's the byte array generated by the third line, let's catch up and have a look

generateClassFile()
Delete some codes, you can see that var14 writes a lot of byte data to var13;
 private byte[] generateClassFile() {
         //Add to hashCode Method
        this.addProxyMethod(hashCodeMethod, Object.class);
        //Add to equals Method
        this.addProxyMethod(equalsMethod, Object.class);
        //Add to toString Method
        this.addProxyMethod(toStringMethod, Object.class);
        Class[] var1 = this.interfaces;
        int var2 = var1.length;      
        //Methods and fields can't exceed 65535, but we usually don't exceed this number when we customize a class
        if (this.methods.size() > 65535) {
            throw new IllegalArgumentException("method limit exceeded");
        } else if (this.fields.size() > 65535) {
            throw new IllegalArgumentException("field limit exceeded");
        } else {
          
            ByteArrayOutputStream var13 = new ByteArrayOutputStream();
            DataOutputStream var14 = new DataOutputStream(var13);

            try {
                var14.writeInt(-889275714);
                var14.writeShort(0);
                var14.writeShort(49);
                this.cp.write(var14);
                var14.writeShort(this.accessFlags);
                var14.writeShort(this.cp.getClass(dotToSlash(this.className)));
                var14.writeShort(this.cp.getClass("java/lang/reflect/Proxy"));
                var14.writeShort(this.interfaces.length);
                Class[] var17 = this.interfaces;
                int var18 = var17.length;

                for(int var19 = 0; var19 < var18; ++var19) {
                    Class var22 = var17[var19];
                    var14.writeShort(this.cp.getClass(dotToSlash(var22.getName())));
                }

                var14.writeShort(this.fields.size());
                var15 = this.fields.iterator();

                while(var15.hasNext()) {
                    ProxyGenerator.FieldInfo var20 = (ProxyGenerator.FieldInfo)var15.next();
                    var20.write(var14);
                }

                var14.writeShort(this.methods.size());
                var15 = this.methods.iterator();

                while(var15.hasNext()) {
                    ProxyGenerator.MethodInfo var21 = (ProxyGenerator.MethodInfo)var15.next();
                    var21.write(var14);
                }

                var14.writeShort(0);
                return var13.toByteArray();
            } 
        }
    }

Now go back to the defineClass0 () method.

 

 

This is a native method, which directly calls the virtual machine method; after that, we can't see how to implement it. I guess defineClass0 () calls the methods in the classloader.

If you are not familiar with classloaders, take a look at this article: java custom class loader , you will find that our custom class loader also reads the class file as a byte array and gives it to the virtual machine.

 

Conclusion:

The proxy object of JDK dynamic agent comes from a byte array;

This array stores the compiled contents;

And the byte array is given to the virtual machine for instantiation.

 
As mentioned above, bytecode can be written to a local file. Now let's implement this function.
//Execution class
public class Main {
    public static void main(String[] args) {
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
        Singer singer=new Singer();
        MethodHandler methodHandler=new MethodHandler(singer);
        IVoice proxy=(IVoice) Proxy.newProxyInstance(singer.getClass().getClassLoader(), singer.getClass().getInterfaces(), methodHandler);
        proxy.song();
    }
}

I use idea to run it here. Go to the root directory of the project.

 

 

It's similar to the class file we usually see. Anyway, we can't understand it.

Use JD GUI to decompile the code:

package com.sun.proxy;

import f.d.IVoice;
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 IVoice {
  private static Method m1;
  
  private static Method m2;
  
  private static Method m3;
  
  private static Method m0;
  
  public $Proxy0(InvocationHandler paramInvocationHandler) {
    super(paramInvocationHandler);
  }
  
  public final boolean equals(Object paramObject) {
    try {
      return ((Boolean)this.h.invoke(this, m1, new Object[] { paramObject })).booleanValue();
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
  
  public final String toString() {
    try {
      return (String)this.h.invoke(this, m2, null);
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
  
  public final Object song() {
    try {
      return this.h.invoke(this, m3, null);
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
  
  public final int hashCode() {
    try {
      return ((Integer)this.h.invoke(this, m0, null)).intValue();
    } catch (Error|RuntimeException error) {
      throw null;
    } catch (Throwable throwable) {
      throw new UndeclaredThrowableException(throwable);
    } 
  }
  
  static {
    try {
      m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
      m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
      m3 = Class.forName("f.d.IVoice").getMethod("song", new Class[0]);
      m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      return;
    } catch (NoSuchMethodException noSuchMethodException) {
      throw new NoSuchMethodError(noSuchMethodException.getMessage());
    } catch (ClassNotFoundException classNotFoundException) {
      throw new NoClassDefFoundError(classNotFoundException.getMessage());
    } 
  }
}

This is what JDK dynamic agent said. I will continue to talk about CGLIB dynamic agent later.

Posted by sareejoh on Wed, 15 Apr 2020 00:53:02 -0700