The introduction
In this article, I will explain the use of agent through several simple procedures, and finally in the actual practice link, I will use ASM bytecode framework to realize a small tool, which is used to collect the parameters and return values of the specified method in the program running. The content about ASM bytecode is not the focus of this article and will not be explained too much. If you do not understand, you can Google it yourself.
Introduction to the
The Java Agent provides a way to modify the bytecode when it is loaded. There are two ways to do this: one is through premain before the main method is executed, and the other is through attach API while the application is running.
Before introducing agent, first give you a simple Instrumentation. It is an API provided by JDK1.5 for intercepting classloading events and modifying bytecodes. Its main methods are as follows: Public interface Instrumentation {// Register a transformer, Void addTransformer(ClassFileTransformer transformer, Boolean canRetransform); Void retransformClasses(Class<? >… classes) throws UnmodifiableClassException; Void redefineClasses(ClassDefinition… definitions) throws ClassNotFoundException, UnmodifiableClassException; }
premain
Premain is the method that runs before main and is the most common agent method. Jar =xunche HelloWorld java-javaAgent :agent.jar=xunche HelloWorld
Premain provides the following two overloaded methods. When the Jvm starts, the first method is tried, and the second method is used if not: public static void premain(String agentArgs, Instrumentation inst); public static void premain(String agentArgs);
A simple example
Premain: Hello World premain: hello World premain: Hello World premain: Hello World premain
package org.xunche.app; public class HelloWorld { public static void main(String[] args) { System.out.println(“Hello World”); }}
Next we look at the agent code, running the premain method and printing the parameters we pass in. package org.xunche.agent; public class HelloAgent { public static void premain(String args) { System.out.println(“Hello Agent: ” + args); }}
To enable the agent to run, we need to package the agent path written by premain-class in the meta-INF/manifest.mf file as a JAR package. Of course, you can also export jar packages directly using IDEA. echo ‘Premain-Class: org.xunche.agent.HelloAgent’ > manifest.mf javac org/xunche/agent/HelloAgent.java javac org/xunche/app/HelloWorld.java jar cvmf manifest.mf hello-agent.jar org/
Next, let’s compile and run the test code. For simplicity, I will be compiled class and agent jar package in the same directory Java – javaagent: hello – agent. The jar = xunche org/xunche/app/HelloWorld
The premain method in the Agent is limited to the main method that executes Hello Agent: xunche Hello World
A slightly more complicated example
Through the above examples, is there a simple understanding of agent?
Let’s take a look at a slightly more complex, we use agent to achieve a method monitoring function. For non-JDK methods, we use ASM to embed several lines of code that record the time stamp at the entry and exit of the method execution. When the method is finished, the time stamp is used to obtain the method’s time.
The logic is simple. The main method calls the sayHi method, prints hi, xunche, and randomly sleeps for a while.
package org.xunche.app; public class HelloXunChe { public static void main(String[] args) throws InterruptedException { HelloXunChe helloXunChe = new HelloXunChe(); helloXunChe.sayHi(); } public void sayHi() throws InterruptedException { System.out.println(“hi, xunche”); sleep(); } public void sleep() throws InterruptedException { Thread.sleep((long) (Math.random() * 200)); }}
接下来我们借助 asm 来植入我们自己的代码,在 jvm 加载类的时候,为类的每个方法加上统计方法调用耗时的代码,代码如下,这里的 asm 我使用了 jdk 自带的,当然你也可以使用官方的 asm 类库。 package org.xunche.agent; import jdk.internal.org.objectweb.asm.*; import jdk.internal.org.objectweb.asm.commons.AdviceAdapter; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; public class TimeAgent { public static void premain(String args, Instrumentation instrumentation) { instrumentation.addTransformer(new TimeClassFileTransformer()); } private static class TimeClassFileTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { if (className.startsWith(“java”) || className.startsWith(“jdk”) || className.startsWith(“javax”) || className.startsWith(“sun”) || className.startsWith(“com/sun”)|| className.startsWith(“org/xunche/agent”)) { //return null或者执行异常会执行原来的字节码 return null; } System.out.println(“loaded class: ” + className); ClassReader reader = new ClassReader(classfileBuffer); ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); reader.accept(new TimeClassVisitor(writer), ClassReader.EXPAND_FRAMES); return writer.toByteArray(); } } public static class TimeClassVisitor extends ClassVisitor { public TimeClassVisitor(ClassVisitor classVisitor) { super(Opcodes.ASM5, classVisitor); } @Override public MethodVisitor visitMethod(int methodAccess, String methodName, String methodDesc, String signature, String[] exceptions) { MethodVisitor methodVisitor = cv.visitMethod(methodAccess, methodName, methodDesc, signature, exceptions); return new TimeAdviceAdapter(Opcodes.ASM5, methodVisitor, methodAccess, methodName, methodDesc); } } public static class TimeAdviceAdapter extends AdviceAdapter { private String methodName; protected TimeAdviceAdapter(int api, MethodVisitor methodVisitor, int methodAccess, String methodName, String methodDesc) { super(api, methodVisitor, methodAccess, methodName, methodDesc); this.methodName = methodName; } @Override protected void onMethodEnter() { //在方法入口处植入 if (“”.equals(methodName)|| “”.equals(methodName)) { return; } mv.visitTypeInsn(NEW, “java/lang/StringBuilder”); mv.visitInsn(DUP); mv.visitMethodInsn(INVOKESPECIAL, “java/lang/StringBuilder”, “”, “()V”, false); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/Object”, “getClass”, “()Ljava/lang/Class;”, false); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/Class”, “getName”, “()Ljava/lang/String;”, false); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/StringBuilder”, “append”, “(Ljava/lang/String;)Ljava/lang/StringBuilder;”, false); mv.visitLdcInsn(“.”); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/StringBuilder”, “append”, “(Ljava/lang/String;)Ljava/lang/StringBuilder;”, false); mv.visitLdcInsn(methodName); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/StringBuilder”, “append”, “(Ljava/lang/String;)Ljava/lang/StringBuilder;”, false); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/StringBuilder”, “toString”, “()Ljava/lang/String;”, false); mv.visitMethodInsn(INVOKESTATIC, “org/xunche/agent/TimeHolder”, “start”, “(Ljava/lang/String;)V”, false); } @Override protected void onMethodExit(int i) { //在方法出口植入 if (“”.equals(methodName) || “”.equals(methodName)) { return; } mv.visitTypeInsn(NEW, “java/lang/StringBuilder”); mv.visitInsn(DUP); mv.visitMethodInsn(INVOKESPECIAL, “java/lang/StringBuilder”, “”, “()V”, false); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/Object”, “getClass”, “()Ljava/lang/Class;”, false); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/Class”, “getName”, “()Ljava/lang/String;”, false); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/StringBuilder”, “append”, “(Ljava/lang/String;)Ljava/lang/StringBuilder;”, false); mv.visitLdcInsn(“.”); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/StringBuilder”, “append”, “(Ljava/lang/String;)Ljava/lang/StringBuilder;”, false); mv.visitLdcInsn(methodName); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/StringBuilder”, “append”, “(Ljava/lang/String;)Ljava/lang/StringBuilder;”, false); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/StringBuilder”, “toString”, “()Ljava/lang/String;”, false); mv.visitVarInsn(ASTORE, 1); mv.visitFieldInsn(GETSTATIC, “java/lang/System”, “out”, “Ljava/io/PrintStream;”); mv.visitTypeInsn(NEW, “java/lang/StringBuilder”); mv.visitInsn(DUP); mv.visitMethodInsn(INVOKESPECIAL, “java/lang/StringBuilder”, “”, “()V”, false); mv.visitVarInsn(ALOAD, 1); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/StringBuilder”, “append”, “(Ljava/lang/String;)Ljava/lang/StringBuilder;”, false); mv.visitLdcInsn(“: “); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/StringBuilder”, “append”, “(Ljava/lang/String;)Ljava/lang/StringBuilder;”, false); mv.visitVarInsn(ALOAD, 1); mv.visitMethodInsn(INVOKESTATIC, “org/xunche/agent/TimeHolder”, “cost”, “(Ljava/lang/String;)J”, false); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/StringBuilder”, “append”, “(J)Ljava/lang/StringBuilder;”, false); mv.visitMethodInsn(INVOKEVIRTUAL, “java/lang/StringBuilder”, “toString”, “()Ljava/lang/String;”, false); mv.visitMethodInsn(INVOKEVIRTUAL, “java/io/PrintStream”, “println”, “(Ljava/lang/String;)V”, false); } } } 上述的代码略长, asm 的部分可以略过。我们通过 instrumentation.addTransformer 注册一个转换器,转换器重写了 transform 方法,方法入参中的 classfileBuffer 表示的是原始的字节码,方法返回值表示的是真正要进行加载的字节码。
The onMethodEnter method calls TimeHolder’s start method and passes in the current method name.
The onMethodExit method calls the cost method of TimeHolder, passing in the current method name, and printing the return value of the cost method.
Look at the code for TimeHolder: package org.xunche.agent; import java.util.HashMap; import java.util.Map; public class TimeHolder { private static Map<String, Long> timeCache = new HashMap<>(); public static void start(String method) { timeCache.put(method, System.currentTimeMillis()); } public static long cost(String method) { return System.currentTimeMillis() – timeCache.get(method); }}
At this point, the agent code has been written, the asm part is not the focus of this chapter, and a separate article about ASM will be published in the future. With the code we monitor embedded at class load time, let’s take a look at what the code looks like with the ASM modification. As you can see, compared to the original test code, each method adds code that counts the time taken by our method. package org.xunche.app; import org.xunche.agent.TimeHolder; public class HelloXunChe { public HelloXunChe() { } public static void main(String[] args) throws InterruptedException { TimeHolder.start(args.getClass().getName() + “.” + “main”); HelloXunChe helloXunChe = new HelloXunChe(); helloXunChe.sayHi(); HelloXunChe helloXunChe = args.getClass().getName() + “.” + “main”; System.out.println(helloXunChe + “: ” + TimeHolder.cost(helloXunChe)); } public void sayHi() throws InterruptedException { TimeHolder.start(this.getClass().getName() + “.” + “sayHi”); System.out.println(“hi, xunche”); this.sleep(); String var1 = this.getClass().getName() + “.” + “sayHi”; System.out.println(var1 + “: ” + TimeHolder.cost(var1)); } public void sleep() throws InterruptedException { TimeHolder.start(this.getClass().getName() + “.” + “sleep”); Thread.sleep ((long) (Math. The random () * 200.0 D)); String var1 = this.getClass().getName() + “.” + “sleep”; System.out.println(var1 + “: ” + TimeHolder.cost(var1)); }}
agentmain
The above premain uses agetN to modify the bytecode before the application starts to implement the desired function. In fact, the JDK provides the Attach API, which allows you to access started Java processes. Class loading is intercepted through the AgentMain method. Let’s look at agentMain in action.
In actual combat
The goal of this exercise is to implement a small tool whose goal is to remotely collect method call information for an already running Java process. If that sounds like BTrace, that’s actually how BTrace is implemented. Just because of the time, the actual combat code is relatively simple, we do not have to pay attention to details, look at the idea of implementation.
The specific implementation ideas are as follows:
The Agent modifies the bytecode of the method of the specified class and collects the input parameter and return value of the method. The server uses the Attach API to access the running Java process and load the Agent. Enable the Agent program to take effect on the target process. The server specifies the classes and methods to be collected when loading the Agent. The server enables a port to receive the request information from the target process