Java agent learning summary

Posted by mohamdally on Sat, 01 Feb 2020 11:42:40 +0100

Preface

Recently, because of the company's needs, we need to understand the java probe, find information on the Internet, and find a lot of data, but there are too few examples. Some of them paste the company code directly, which is too complex, and some of them are particularly simple, which is not what I want. I want an example like this:

The jvm is running. I want to dynamically modify a class. The jvm automatically loads a new class definition without restarting. It's cool to dynamically modify the class definition. This paper will implement a method monitoring example. The starting method is not monitored. After the dynamic modification, the method will take time to print after the method execution

Introduction to Instrumentation

With Instrumentation, developers can build an application independent Agent to monitor and assist programs running on the JVM, and even replace and modify the definition of some classes. With this function, developers can realize more flexible runtime virtual machine monitoring and Java class operation. In fact, this feature provides an AOP implementation mode supported by virtual machine level, which enables developers to realize some AOP functions without any upgrade and change to JDK.

In Java SE 5, Instrument requires to use command-line parameters or system parameters to set the proxy class before running. In actual running, when the virtual machine initializes (before most Java class libraries are loaded), it starts the setting of Instrument, so that the definition of class can be modified before loading bytecode.

In Java SE6, it goes further. When the jvm is running, it is more convenient to dynamically modify the class definition. This article mainly talks about a way

The Instrumentation class is defined as follows:

 1 /*There are two ways to get an instance of the Instrumentation interface:
 2 1.When the JVM is started in a manner that indicates a proxy class. In this case, pass the Instrumentation instance to the main method of the proxy class.
 3 2. JVM Provides a mechanism to start the agent at a certain time after the JVM starts. In this case, pass the Instrumentation instance to the agent code's agentmain method.
 4 These mechanisms are described in the packaging specification.
 5 After the agent gets an instance of Instrumentation, the agent can call methods on that instance at any time.
 6 */
 7 public interface Instrumentation {
 8     //Add one more Class Converter for files, converter for changes Class Binary stream data, parameters canRetransform Sets whether retransmissions are allowed.
 9     void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
10     //Register a converter
11     void addTransformer(ClassFileTransformer transformer);
12 
13     //Delete a class converter
14     boolean removeTransformer(ClassFileTransformer transformer);
15 
16     boolean isRetransformClassesSupported();
17 
18     //Redefining after class loading Class. This is very important. The method is 1.6 In fact, the method is update Class.
19     void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
20 
21     boolean isRedefineClassesSupported();
22     /*This method is used to replace the definition of the class without referring to the existing class file bytes, and will not cause any initialization except for the initialization that will occur under the normal JVM semantics. In other words, redefining a class does not cause its initializer to run. The value of the static variable remains as it was before the call.
23 Instances of the redefined class are not affected.*/
24     void redefineClasses(ClassDefinition... definitions)
25         throws  ClassNotFoundException, UnmodifiableClassException;
26 
27     boolean isModifiableClass(Class<?> theClass);
28     //Get all loaded classes
29     @SuppressWarnings("rawtypes")
30     Class[] getAllLoadedClasses();
31 
32     @SuppressWarnings("rawtypes")
33     Class[] getInitiatedClasses(ClassLoader loader);
34     //Get the size of an object
35     long getObjectSize(Object objectToSize);
36    
37     void appendToBootstrapClassLoaderSearch(JarFile jarfile);
38     
39     void appendToSystemClassLoaderSearch(JarFile jarfile);
40     boolean isNativeMethodPrefixSupported();
41     void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
42 }
  • addTransformer and retransformClasses are related. addTransformer registers the converter and retransformClasses triggers the converter
  • Redefiniteclass is another way to transform a class definition in addition to Transformer

Two ways of Instrument

First: static Instrument before JVM startup

Start the agent using the javaagent command. The parameter javaagent can be used to specify a jar package, and there are two requirements for the java package:

  1. The MANIFEST.MF file of this jar package must specify the premain class entry.
  2. The class specified by premain class must implement the premain() method.

The premain method, literally, is the class running before the main function. When the Java virtual machine starts, before executing the main function, the JVM will run the premain method of the class premain class in the jar package specified by - javaagent.

Enter java on the command line to see the corresponding parameters, including those related to java agent:

-Agentlib: < libname > [= < Options >] load the native agent library < libname >, for example - agentlib:hprof
    See also - agentlib:jdwp=help and - agentlib:hprof=help
 -Agentpath: < pathname > [= < Options >]
    Load native agent library by full pathname
 -Javaagent: < jarpath > [= < Options >]
    To load the Java programming language agent, see java.lang.instrument

In essence, Java Agent is a regular Java class that follows a set of strict conventions. As mentioned above, the javaagent command requires that there must be a premain() method in the specified class, and there are also requirements for the signature of the premain method. The signature must meet the following two formats:

public static void premain(String agentArgs, Instrumentation inst)
    
public static void premain(String agentArgs)

The JVM will give priority to loading methods with Instrumentation signature. The second method will be ignored if loading succeeds. If the first method is not available, the second method will be loaded. This logic is in the sun.instrument.InstrumentationImpl class

How to use Java agent?

Using Java agent requires several steps:

  1. To define a MANIFEST.MF file, you must include the main class option, and can redefine classes and can retransform classes options are usually added.
  2. Create a class specified by premain class. The class contains the premain method. The method logic is determined by the user.
  3. jar the premain class and MANIFEST.MF file.
  4. Use the parameter - javaagent: jar package path to start the method to be proxied.

After performing the above steps, the JVM will first execute the premain method, which will be used for most class loading. Note: most, not all. Of course, the main omission is system class, because many system classes execute before agent, and the loading of user class will be blocked. That is to say, this method intercepts the loading activities of most classes before the main method is started. Since it can intercept the loading of classes, it can do such operations as rewriting classes, combining with the third-party bytecode compilation tools, such as ASM, javassist, cglib, etc., to rewrite implementation classes.

Common configuration of the manifest.mf file:

Premain class: the class containing the premain method (full path name of the class)

Agent class: the class containing the agentmain method (full pathname of the class)

Boot class path: sets the path list for the boot class loader search. After a platform specific mechanism to find a class fails, the bootstrap classloader searches these paths. Search paths in the order listed. The paths in the list are separated by one or more spaces. Path uses the path component syntax of a hierarchical URI. If the path begins with a slash character ("/"), it is an absolute path, otherwise it is a relative path. The relative path is resolved according to the absolute path of the proxy JAR file. Ignore malformed paths and nonexistent paths. If the agent starts at some point after VM starts, the path that does not represent the JAR file is ignored. (optional)

Can redefine classes: true indicates that the classes required by this agent can be redefined. The default value is false (optional)

Can retransform classes: true indicates that the classes required by this agent can be retransmitted. The default value is false (optional)

Can set native method prefix: true indicates that the native method prefix required by this agent can be set. The default value is false (optional)

Give an example of premain:

 1 public class PreMainTraceAgent {
 2     public static void premain(String agentArgs, Instrumentation inst) {
 3         System.out.println("agentArgs : " + agentArgs);
 4         inst.addTransformer(new DefineTransformer(), true);
 5     }
 6 
 7     static class DefineTransformer implements ClassFileTransformer{
 8         @Override
 9         public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
10             System.out.println("premain load Class:" + className);
11             return classfileBuffer;
12         }
13     }
14 }

Because this article doesn't focus on this static Instrumentation method, it's just a brief introduction. You can search for the interested ones

The second way of dynamic Instrumentation

In the Instrumentation of Java SE 6, there is a "agentmain" method "that" keeps pace with premain ", which can be run after the main function starts to run.

Like the premain function, developers can write a Java class with the "agentmain" function:

Because this article doesn't focus on this static Instrumentation method, it's just a brief introduction. You can search for the interested ones
 The second way of dynamic Instrumentation

In the Instrumentation of Java SE 6, there is a "agentmain" method "that" keeps pace with premain ", which can be run after the main function starts to run.
Like the premain function, developers can write a Java class with the "agentmain" function:

Like the premain function, developers can perform various operations on classes in agentmain. The usage of agentArgs and Inst is the same as that of premain.

Similar to "premain class", developers must set "agent class" in the manifest file to specify the class containing the agentmain function.

However, unlike premain, agentmain needs to be started after the main function starts running. As for how to run this method and how to associate it with the running jvm, you need to introduce the Attach API

The Attach API is not a standard Java API, but a set of extension APIs provided by Sun company to "Attach" the agent utility to the target JVM. With it, developers can easily monitor a JVM and run an additional agent.

The Attach API is very simple. There are only two main classes in the com.sun.tools.attach package: VirtualMachine represents a Java virtual machine, which is the target virtual machine that the program needs to monitor. It provides JVM enumeration, Attach action and Detach action (opposite behavior of Attach action, removing a proxy from the JVM), etc; VirtualMachineDescriptor is a container class that describes the virtual machine. It cooperates with VirtualMachine class to complete various functions.

Next, we use the example above to realize the time-consuming execution of a monitoring method: to execute a method regularly, the starting method is not monitored, and the method redefinition plus monitoring.

A simple method monitoring example

So let's think about the implementation of this example, which requires several modules

  • An agent module (monitoring logic);
  • A main function (running jvm);
  • A program that associates the two modules above

Starting with the agent module:

1. TimeTest class to be monitored:

/**
 * @ClassName TimeTest
 * @Author jiangyuechao
 * @Date 2020/1/20-10:36
 * @Version 1.0
 */
public class TimeTest {

    public static void sayHello( ){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sayhHello..........");
    }

    public static void sayHello2(String word){
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sayhHello2.........."+word);
    }
}

2. Write agent code

Bytecode conversion class:

 1 public class MyTransformer implements ClassFileTransformer {
 2 
 3     // List of methods processed
 4     final static Map<String, List<String>> methodMap = new HashMap<String, List<String>>();
 5 
 6     public MyTransformer() {
 7         add("com.chaochao.java.agent.TimeTest.sayHello");
 8         add("com.chaochao.java.agent.TimeTest.sayHello2");
 9     }
10 
11     private void add(String methodString) {
12         String className = methodString.substring(0, methodString.lastIndexOf("."));
13         String methodName = methodString.substring(methodString.lastIndexOf(".") + 1);
14         List<String> list = methodMap.get(className);
15         if (list == null) {
16             list = new ArrayList<String>();
17             methodMap.put(className, list);
18         }
19         list.add(methodName);
20     }
21 
22     @Override
23     public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
24                             ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
25         System.out.println("className:"+className);
26         if (methodMap.containsKey(className)) {// Judge loaded class Is the package path of the need to monitor the class
27             try {
28                 ClassPool classPool=new ClassPool();
29                 classPool.insertClassPath(new LoaderClassPath(loader));
30                 CtClass ctClass= classPool.get(className.replace("/","."));
31 //                CtMethod ctMethod= ctClass.getDeclaredMethod("run");
32                 CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
33                 for (CtMethod ctMethod : declaredMethods) {
34                        //Insert local variable
35                     ctMethod.addLocalVariable("begin",CtClass.longType);
36                     ctMethod.addLocalVariable("end",CtClass.longType);
37 
38                     ctMethod.insertBefore("begin=System.currentTimeMillis();System.out.println(\"begin=\"+begin);");
39                     //Insert before: the last insert is placed at the top
40                     ctMethod.insertBefore("System.out.println( \"Start of burying point-1\" );");
41 
42                     ctMethod.insertAfter("end=System.currentTimeMillis();System.out.println(\"end=\"+end);");
43                     ctMethod.insertAfter("System.out.println(\"performance:\"+(end-begin)+\"Millisecond\");");
44 
45                     //Insert after: the last insert is placed at the bottom
46                     ctMethod.insertAfter("System.out.println( \"End of burial point-1\" );");
47                 }
48                 return ctClass.toBytecode();
49             }  catch (NotFoundException | CannotCompileException|IOException e) {
50                 e.printStackTrace();
51             } 
52             return new byte[0];
53         }
54         else
55              System.out.println("Can't find.");
56         return null;
57     }
58     
59 }

The class above is to add time-consuming printing before and after the method

Below is the defined agent main test:

import java.lang.instrument.Instrumentation;

public class AgentMainTest {
   //Methods executed after Association
    public static void agentmain(String args, Instrumentation inst) throws Exception {
        System.out.println("Args:" + args);
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clazz : classes) 
        {
           System.out.println(clazz.getName());
        }
        System.out.println("Start customization MyTransformer");
        // Add to Transformer
        inst.addTransformer(new MyTransformer(),true);
        
        inst.retransformClasses(TimeTest.class);
    }
    
    public static void premain(String args, Instrumentation inst) throws Exception 
    {
        System.out.println("Pre Args:" + args);
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clazz : classes) 
        {
           System.out.println(clazz.getName());
        }
    } 
}

Manaifrest.mf file definition, note that the last line is a space:

Manifest-Version: 1.0
Premain-Class: com.chaochao.java.agent.AgentMainTest
Agent-Class: com.chaochao.java.agent.AgentMainTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true

 

Agent module introduction is finished, below is a main function program. This is very simple

 1 public class TestMan {
 2 
 3     public static void main(String[] args) throws InterruptedException 
 4     {
 5         TimeTest tt = new TimeTest();
 6         tt.sayHello();
 7         tt.sayHello2("one");
 8         while(true)
 9         {
10             Thread.sleep(60000);
11             new Thread(new WaitThread()).start();  
12             tt.sayHello();
13             tt.sayHello2("two");
14         }
15     }
16      
17    static class WaitThread implements Runnable 
18    {
19         @Override  
20         public void run()
21         {
22             System.out.println("Hello"); 
23         }
24    }
25 }

Last correlation module:

/**
 * 
 * @author jiangyuechao
 *
 */
public class AttachMain {

    public static void main(String[] args) throws Exception{
        VirtualMachine vm = null;  
        String pid = null;
        List<VirtualMachineDescriptor> list = VirtualMachine.list();  
        for (VirtualMachineDescriptor vmd : list)  
        {
            System.out.println("pid:" + vmd.id() + ":" + vmd.displayName());
            if(vmd.displayName().contains("TestMan")) {
                pid = vmd.id();
            }
        }
        //E:\eclipse-workspace\JavaStudyAll\JVMStudy\target
       // String agentjarpath = "E:/jee-workspace/javaAgent/TestAgent.jar"; //agentjar Route  
        String agentjarpath = "E:/jee-workspace/javaAgent/AgentMainTest.jar"; //agentjar Route  
        vm = VirtualMachine.attach(pid);//target JVM Process ID(PID)  
        vm.loadAgent(agentjarpath, "This is Args to the Agent.");  
        vm.detach();  
      }

}

It is also very simple. The first step is to obtain pid, and the second step is to associate jvm with attach method

Now that the code is ready, how to run it requires several steps:

  1. First, package agent code into jar package
  2. Run main function and execute agent

agent packing

Just pack the agent code into a normal jar package. You can use eclipse or intellij. Take eclipse as an example, just pay attention to using your written manifest file

But I recommend another way, command line, which is convenient and fast

First, put the required classes in a folder, and javac compiles:

javac -encoding UTF-8 -classpath .;E:\tools\jdk1.8.0_65\lib\tools.jar;E:\eclipse-workspace\JavaStudyAll\JVMStudy\lib\javassist.jar; AgentMainTest.java MyTransformer.java

It depends on tools.jar and javassist jar package

The compiled class file is packaged as a jar package:

jar cvmf MANIFEST.MF AgentMainTest.jar AgentMainTest.class MyTransformer.class 

As follows:

After the agent package is ready, it is simple. Run the main function first and start a virtual machine. Run as follows:

sayhHello..........
sayhHello2..........one

Run AttachMain class and associate with agent program, you will see the following output:

You can see that after the method execution, there has been a time-consuming printing. The test is successful

Limitations of Instrumentation

In most cases, we use the function of bytecode Instrumentation, or in general, the function of class redefinition, but it has the following limitations:

  1. When the bytecode is modified in two ways, premain and agentmain, it is after the Class file is loaded, that is to say, it must have Class type parameters. You cannot redefine a Class that does not exist through the bytecode file and the user-defined Class name.
  2. The bytecode modification of a class is called a class transform. In fact, class transformations always return to the class redefinition instrumentation ා redefineclasses() method. This method has the following limitations:
    1. The parents of the new class and the old class must be the same;
    2. The number of interfaces implemented by the new class and the old class should be the same, and they are the same interfaces;
    3. The new and old class accessors must be the same. The number and name of new and old fields should be consistent;
    4. The methods added or deleted by new and old classes must be modified by private static/final;
    5. You can modify the method body.

In addition to the above method, if you want to redefine a class, you can consider the method based on classloader isolation: create a new custom classloader to define a new class through a new bytecode, but there is also the limitation that you can only call the new class through reflection.

Reference resources:

https://www.cnblogs.com/rickiyang/p/11368932.html

https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html 

 

Please indicate the source of forwarding: https://www.cnblogs.com/jycboy/p/12249472.html 

Topics: Java jvm Eclipse JDK