Abstract:Java-agent is a Trace Tool for Java. At its core, it is a call to JVMTI (JVM Tool Interface).

This article is from The Huawei Cloud community Java Dynamic Trace Technology: Java-Agent, the original author: technology torchbearer.

Dynamic Trace is a technique that monitors the invocation of the program after the application is deployed, gets the contents of variables in it, and even inserts or replaces parts of the code. There are many trace tools in the industry, such as PTrace, Strace, eBPF, BTrace, Java-agent, etc. The purpose of this application is to monitor publish and Consume calls in Kafka and capture dependencies. Since Kafka is written in Scala, it uses Java-Agent technology.

Java-agent is a Trace Tool for Java. At its core, it is a call to JVMTI (JVM Tool Interface). JVMTI is a series of interface functions of the Java Virtual machine. You can obtain the current running status of the Java virtual machine through JVMTI. When the Java-Agent program is running, an Agent process is mounted to the Java VIRTUAL Machine (VM) and the mounted Java applications are monitored through JVMTI. Agent program can complete Java code hot replacement, class loading process monitoring and other functions.

The java-Agent can be mounted in two modes: static and dynamic. In static mounting, the Agent is started together with Java applications. Before Java applications are initialized, the Agent is mounted and starts to monitor Java applications. Dynamic mounting is to dynamically mount the Agent to the target process when the application is running. The object to be mounted is determined by the process ID.

Static mount

First, write a monitor program for Java-Agent, statically mount the entry function for premain. There are two types of premain, the difference being that the parameters are passed in. The Instrumentation parameter is usually selected and can be used to perform hot replacement of the code.

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

Here’s 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 an implementation of ClassFileTransformer. The input to its transform function gives the name of the class being loaded, the class loader, and so on. 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; }}}

To run java-agent, you need to package the above programs into a JAR file, which contains the following items in the MANIFEst.mf of the JAR file

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

Premain-class specifies the Class in which the jar’s Premain function resides. When the java-agent loads the jar package, it looks for Premain in the Premain Class. Can-Redefine-Classes and Can- Retransform-classes are set to true, allowing the program to modify Java application code.

If you are using a Maven project, you can add manifest.mf automatically 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>

After you output the JAR file, write a Hello World Java application that compiles to Hello.class, and start the application with the following command

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

In execution, you can print all the classes loaded by the Java virtual Machine when it runs Hello.class.

The functionality of the Java-Agent is not limited to the output class loading process. The following example provides a 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; }}

Then we modify the Transformer in the PreMain class and add it with Instrumentation. Same as 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 class being loaded is the test class we wrote, Enter modify. If ("com/huawei/App".equals(className)) {try {// obtain the CtClass object from the 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()); CtMethod convertToAbbr = clazz.getdeclAredMethod ("add"); CtMethod convertToAbbr = clazz.getdeclAredMethod ("add"); String methodBody = "{return $1 + $2 + 11; } "; convertToAbbr.setBody(methodBody); String methodBody = "system.out.println (integer.toString ($1));"; ; convertToAbbr.insertBefore(methodBody); Byte [] byteCode = clazz.tobyTecode (); // 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 it is not found in memory next time, it will be reloaded by Javassist. Detach (); return byteCode; } catch (Exception ex) { ex.printStackTrace(); } // If null is returned, the bytecode will not be modified. }}

The next step is the same as before. When you run it, you will find that the logic of the add function has been replaced.

Dynamic mount

Dynamic mounting means that agents are dynamically added while applications are running. The technical principle is to communicate with the target process through the socket, and send the load instruction to mount the specified JAR file in the target process. The function of agent execution is the same as that of static overload. During implementation, there are several differences. First, the entry function name is different. The dynamically mounted function name is AgentMain. Like Premain, there are two formats. But usually with the Instrumentation. 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 class being loaded is the test class we wrote, Enter modify. If ("com/huawei/App".equals(className)) {try {// obtain the CtClass object from the 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()); CtMethod convertToAbbr = clazz.getdeclAredMethod ("add"); CtMethod convertToAbbr = clazz.getdeclAredMethod ("add"); String methodBody = "{return $1 + $2 + 11; } "; convertToAbbr.setBody(methodBody); Byte [] byteCode = clazz.tobyTecode (); // 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 it is not found in memory next time, it will be reloaded by Javassist. Detach (); return byteCode; } catch (Exception ex) { ex.printStackTrace(); } // If null is returned, the bytecode will not be modified. }}}

It has the same function as static loading. Note that with transformer added, Instrumentation calls the retransformClasses function. This is because Transformer is only called when the class is loaded by the Java virtual Machine. If you load dynamically, the class files you want to monitor may have already been loaded. So you need to call retransformClasses to reload.

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

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

The final difference is the loading method. Dynamic mount 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 starting the class name. Obtain the VirtualMachine instance based on the process ID and load the Jar file of AgentMain.

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(); }}}}

Scala Program Monitoring

Scala is compatible with Java, so it is possible to monitor Scala applications using java-Agents. But there are still some caveats. The first is that program substitutions only apply to classes, not to objects. The second problem is that in dynamic substitution, the program is compiled into bytecode and then replaced. Java-agent uses Java’s compilation rules, so the replacement must use Java’s language rules, otherwise a compilation error will occur. For example, the example uses System.out.println to output parameter information. If you use Scala’s println, there will be a compilation error.

References:

Java Dynamic debugging technology Principles and practices JavaAgent Usage Guide

Click follow to learn about the fresh technologies of Huawei Cloud