Abstract: Java-Agent is a trace Tool applied to Java, the core is the call to JVMTI (JVM Tool Interface).

This article is shared by Huawei cloud community “Java Dynamic Trace Technology: Java-Agent”, originally written by: technology torch bearer.

Dynamic trace technology monitors application calls after application deployment, retrieves variable contents, and can even insert or replace parts of code. There are many trace tools in the industry, such as PTrace, Strace, eBPF, bTrace, Java-Agent and so on. The purpose of this application is to monitor publish and consume calls in Kafka services and obtain dependencies. Since Kafka is written in Scala, java-Agent technology is used.

Java-agent is a Trace Tool applied to Java, the core of which is to call JVMTI (JVM Tool Interface). JVMTI is a series of interface functions provided by Java virtual machines (VMS). You can use JVMTI to obtain the running status of a Java VM. When the Java-Agent program runs, an Agent process is mounted to the Java VM, and the mounted Java applications are monitored using JVMTI. Agent program can be completed through the Java code hot replacement, class loading process monitoring and other functions.

Java-agent can be mounted in two modes: static mount and dynamic mount. In static mounting, the Agent starts with the Java application. Before the Java application is initialized, the Agent is mounted and monitors the Java application. In dynamic mount mode, the agent is dynamically mounted to the target process after the mount object is determined by the process ID during application running.

Static mount

First, write the monitor program of Java-Agent, and the entry function of static mount is Premain. There are two kinds of premain functions, the difference being that they take different arguments. An Instrumentation parameter is usually selected, which can be used to complete the hot replacement of code.

public static void premain(String agentArgs, Instrumentation inst);
public static void premain(String agentArgs);
Copy the code

Here is a simple example. In the premain function, add a Transformer using Instrumentation. Transformer is called every time a monitored Java application loads a class. DefineTransformer is a transformer and is an implementation of ClassFileTransformer. The input parameter of its transform function gives the name of the currently loaded class, the classloader, and other information. In the sample we just print the name of the loaded class.

import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import javassist.*; public class PreMain { public static void premain(String agentArgs, Instrumentation inst) { System.out.println("agentArgs : " + agentArgs); inst.addTransformer(new DefineTransformer(), true); } static class DefineTransformer implements ClassFileTransformer{ @Override public byte[] transform(ClassLoader loader, String className, Class<? > classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer){ System.out.println("premain load Class:" + className); return classfileBuffer; }}}Copy the code

To run java-Agent, you need to package the above programs into a JAR file. The manifest.mf of the JAR file should contain the following items

Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.huawei.PreMain
Copy the code

Premain-class declares the Class in which the jar’s Premain function resides. Java-agent will look for Premain in the Premain Class when loading the JAR package. Can-re-re-classes and can-retransform-classes are declared true to allow the program to modify Java application code.

If you are a project using Maven, you can automatically add the manifest.mf by adding the following plugin

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> The < version > 2.6 < / version > < configuration > < appendAssemblyId > false < / appendAssemblyId > < descriptorRefs > <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class>com.huawei.PreMain</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> <executions> <execution> <id>assemble-all</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>Copy the code

After exporting the jar file, write a Hello World Java application and compile it into Hello.class, using the following command when starting the application

Java - javaagent: / root/Java - Agent - Project - the Path/target/JavaAgentTest - 1.0 - the SNAPSHOT. Jar helloCopy the code

All classes loaded by the Java virtual machine running Hello.class can be printed at execution.

Java-agent’s capabilities are not limited to the loading process of output classes. The following example can be used to implement hot replacement of code. Start by writing a test class.

public class App { public static void main( String[] args ) { try{ System.out.println( "main start!" ); App test = new App(); int x1 = 1; int x2 = 2; while(true){ System.out.println(Integer.toString(test.add(x1, x2))); Thread.sleep(2000); } } catch (InterruptedException e) { e.printStackTrace(); System.out.println("main end"); } } private int add(int x1, int x2){ return x1+x2; }}Copy the code

Then we modify the Transformer in the PreMain class and add the Transformer through Instrumentation. Same with DefineTransformer.

static class MyClassTransformer implements ClassFileTransformer { @Override public byte[] transform(final ClassLoader loader, final String className, final Class<? > classBeingRedefined, final ProtectionDomain ProtectionDomain, final byte[] classfileBuffer) {// If the currently loaded class is the test class we wrote, The modification page is displayed. If ("com/huawei/App". Equals (className)) {try {// Get CtClass objects from ClassPool final ClassPool ClassPool = ClassPool.getDefault(); final CtClass clazz = classPool.get("com.huawei.App"); CtMethod[] methodList = clazz.getDeclaredMethods(); for(CtMethod method: methodList){ System.out.println("premain method: "+ method.getName()); $1 = CtMethod convertToAbbr = clazz.getDeclaredMethod("add"); String methodBody = "{return $1 + $2 + 11; } "; convertToAbbr.setBody(methodBody); MethodBody = "system.out.println (integ.toString ($1));" ; convertToAbbr.insertBefore(methodBody); // Return the byteCode and detachCtClass object byte[] byteCode = clazz.tobytecode (); // Detach means to remove the Date object from memory that was loaded by Javassist. If you need to find it in memory the next time, you can re-load clazz.detach() from javassist. return byteCode; } catch (Exception ex) { ex.printStackTrace(); } // If null is returned, the bytecode will not be modified. }}Copy the code

The next step is the same as before, and the logic of the add function has been replaced.

Dynamic mount

Dynamic mount is used to dynamically add agents during application running. The technical principle is to communicate with the target process through socket and send load instruction to mount the specified JAR file in the target process. The function of agent execution is exactly the same as static overload. There are several differences in implementation. First, the entry function name is different. The dynamically mounted function name is AgentMain. Like Premain, there are two formats. But the one with Instrumentation is usually used. As shown in the following example

public class AgentMain { public static void agentmain(String agentArgs, Instrumentation instrumentation) throws UnmodifiableClassException { instrumentation.addTransformer(new MyClassTransformer(), true); instrumentation.retransformClasses(com.huawei.Test.class); } static class MyClassTransformer implements ClassFileTransformer { @Override public byte[] transform(final ClassLoader loader, final String className, final Class<? > classBeingRedefined, final ProtectionDomain ProtectionDomain, final byte[] classfileBuffer) {// If the currently loaded class is the test class we wrote, The modification page is displayed. If ("com/huawei/App". Equals (className)) {try {// Get CtClass objects from ClassPool final ClassPool ClassPool = ClassPool.getDefault(); final CtClass clazz = classPool.get("com.huawei.App"); CtMethod[] methodList = clazz.getDeclaredMethods(); for(CtMethod method: methodList){ System.out.println("premain method: "+ method.getName()); $1 = CtMethod convertToAbbr = clazz.getDeclaredMethod("add"); String methodBody = "{return $1 + $2 + 11; } "; convertToAbbr.setBody(methodBody); // Return the byteCode and detachCtClass object byte[] byteCode = clazz.tobytecode (); // Detach means to remove the Date object from memory that was loaded by Javassist. If you need to find it in memory the next time, you can re-load clazz.detach() from javassist. return byteCode; } catch (Exception ex) { ex.printStackTrace(); } // If null is returned, the bytecode will not be modified. }}}Copy the code

The function is the same as static loading. Note that after Instrumentation adds transformer, the retransformClasses function is called. This is because Transformer is only called when the Class is loaded by the Java virtual machine. If dynamic loading is used, the class files that need to be monitored may already be loaded. So you need to call retransformClasses to reload.

Another difference is that the manifest.mf file requires the addition of an agent-class, as shown below

Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.huawei.PreMain
Agent-Class: com.huawei.AgentMain
Copy the code

The final difference is the loading method. Dynamic mounting requires writing a load script. In this script, as shown below, you first iterate through all the Java processes, identifying the processes you want to monitor by launching the class name. Obtain the VirtualMachine instance using the process ID and load the AgentMain JAR file.

import com.sun.tools.attach.*; import java.io.IOException; import java.util.List; public class TestAgentMain { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException AgentInitializationException {/ / get the current System of all running virtual machine System. Out. The println (" start "running JVM); List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { System.out.println(vmd.displayName()); String aim = "com.huawei.App"; if (vmd.displayName().endsWith(aim)) { System.out.println(String.format("find %s, process id %s", vmd.displayName(), vmd.id())); VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); VirtualMachine. LoadAgent ("/root/Java - Agent - Project - the Path/target/JavaAgentTest - 1.0 - the SNAPSHOT. Jar "); virtualMachine.detach(); }}}}Copy the code

Scala program Monitoring

Scala is compatible with Java, so it is possible to monitor Scala applications using java-Agent. But there are still some issues to be aware of. The first point is that program substitution only works with classes, not objects. The second problem is that dynamic substitution is done after the program is compiled into bytecode. Java-agent uses Java compilation rules, so the replacement program must use Java language rules, otherwise a compilation error will occur. For example, in the example

System.out.println

Output parameter information. If scala’s println is used, a compilation error will occur.

References:

Java dynamic debugging principles and practices JavaAgent Usage Guide

Click to follow, the first time to learn about Huawei cloud fresh technology ~