reference material
ASM dependency not found at compile time
Used[Arthas]As you all know, Arthas is a very powerful Java diagnostic tool open source for Alibaba.
Whether online or offline, we can use Arthas to analyze the thread status of the program, view the real-time running status of the JVM, print the in and out parameters and return types of methods, and collect the time consumption of each code block in the method,
It can even monitor the call times, success times, failure times, average response time and failure rate of classes and methods.
When learning Java Dynamic bytecode technology a few days ago, I suddenly thought of the trace function of this Java diagnostic tool: it takes time to call each node in the print method. Simple, just for the dynamic byte code learning demo.
Program structure
src
├── agent-package.bat
├── java
│ ├── asm
│ │ ├── MANIFEST.MF
│ │ ├── TimerAgent.java
│ │ ├── TimerAttach.java
│ │ ├── TimerMethodVisitor.java
│ │ ├── TimerTrace.java
│ │ └── TimerTransformer.java
│ └── demo
│ ├── MANIFEST.MF
│ ├── Operator.java
│ └── Test.java
├── run-agent.bat
├── target-package.bat
└── tools.jar
Write target program
code
package com.gravel.demo.test.asm;
/**
* @Auther: syh
* @Date: 2020/10/12
* @Description:
*/
public class Test {
public static boolean runnable = true;
public static void main(String[] args) throws Exception {
while (runnable) {
test();
}
}
//Objective: to analyze the time consumption of each node in this method
public static void test() throws Exception {
Operator.handler();
long time_wait = (long) ((Math.random() * 1000) + 2000);
Operator.callback();
Operator.pause(time_wait);
}
}
Operator.java
/**
* @Auther: syh
* @Date: 2020/10/28
*@ Description: auxiliary class, which can also be used to analyze time consumption
*/
public class Operator {
public static void handler() throws Exception {
long time_wait = (long) ((Math.random() * 10) + 20);
sleep(time_wait);
}
public static void callback() throws Exception {
long time_wait = (long) ((Math.random() * 10) + 20);
sleep(time_wait);
}
public static void pause(long time_wait) throws Exception {
sleep(time_wait);
}
public static void stop() throws Exception {
Test.runnable = false;
System.out.println("business stopped.");
}
private static void sleep(long time_wait) throws Exception {
Thread.sleep(time_wait);
}
}
MANIFEST.MF
to write MANIFEST.MF File, specify main class. Note: a colon is followed by a space and two blank lines are added at the end.
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: syh
Created-By: Apache Maven
Build-Jdk: 1.8.0_202
Main-Class: com.gravel.demo.test.asm.Target
pack
Lazy write bat batch command, generate target.jar
@echo off & setlocal
attrib -s -h -r -a /s /d demo
rd /s /q demo
rd /q target.jar
javac -encoding utf-8 -d . ./java/demo/*.java
jar cvfm target.jar ./java/demo/MANIFEST.MF demo
rd /s /q demo
pause
java -jar target.jar
Java agent probe
Instrument is a class library provided by the JVM to modify the loaded class file. In order to modify the code, we need to implement an instrument agent.
In JDK1.5, the agent has an internal method premain. Is modified before the class is loaded. Therefore, it is impossible to modify the running class.
After JDK1.6, agent added agent main method. Agentmain is loaded after the virtual machine is started. So we can do interception, hot deployment, etc.
Speaking of Java probe technology, in fact, I am also half baked. So here I use the idea of exploring while analyzing other people’s examples to realize my simple trace function.
The example uses ASM bytecode generation framework
MANIFEST.MF
First of all, a usable jar, one of the keys is MAINFEST.MF Documents, right.
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: syh
Build-Jdk: 1.8.0_202
Agent-Class: asm.TimerAgent
Can-Retransform-Classes: true
Can-Redefine-Classes: true
Class-Path: ./tools.jar
Main-Class: asm.TimerAttach
We start from MANIFEST.MF Several key attributes are extracted from
|
explain |
Agent-Class |
Agentmain entry class
|
Premain-Class |
Premain entry class, at least one with agent class. |
Can-Retransform-Classes |
The class that has been triggered is redefined. |
Can-Redefine-Classes |
Instead of converting the loaded classes, the bytecode is directly sent to the JVM |
Class-Path |
Technical dependence of ASM dynamic bytecode tools.jar If not, you can copy it from the Lib directory of JDK. |
Main-Class |
This is not a key attribute of the agent. For convenience, I merged the program that loads the virtual machine with the agent. |
code
Then let’s take a look at two entry classes. First, we will analyze the main class of an executable jar.
public class TimerAttach {
public static void main(String[] args) throws Exception {
/**
*When starting jar, you need to specify two parameters: 1 PID of the target program. 2. Class path, method and format to be modified package.class#methodName
*/
if (args.length < 2) {
System.out.println("pid and class must be specify.");
return;
}
if (!args[1].contains("#")) {
System.out.println("methodName must be specify.");
return;
}
VirtualMachine vm = VirtualMachine.attach(args[0]);
//Here, for convenience, I integrate VM and agent into a jar. Args [1] is the input parameter of agentmain.
vm.loadAgent("agent.jar", args[1]);
}
}
The code is very simple, 1: args input parameter check; 2: load target process PID (args [0]); 3: load agent jar package (because of the merge, the jar is actually itself).
among vm.loadAgent ( agent.jar , args [1]) will call the agentmain method in the agent class, and args [1] is the first input parameter of agentmain.
public class TimerAgent {
public static void agentmain(String agentArgs, Instrumentation inst) {
String[] ownerAndMethod = agentArgs.split("#");
inst.addTransformer(new TimerTransformer(ownerAndMethod[1]), true);
try {
inst.retransformClasses(Class.forName(ownerAndMethod[0]));
System.out.println("agent load done.");
} catch (Exception e) {
e.printStackTrace();
System.out.println("agent load failed!");
}
}
}
In the agentmain method, we call the retransformclasses method to load the target class, and call the addtransformer method to load the timertransformer class to redefine the target class.
Class converter
public class TimerTransformer implements ClassFileTransformer {
private String methodName;
public TimerTransformer(String methodName) {
this.methodName = methodName;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classFileBuffer) {
ClassReader reader = new ClassReader(classFileBuffer);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new TimerTrace(Opcodes.ASM5, classWriter, methodName);
reader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
return classWriter.toByteArray();
}
}
Modify the methods in the matched class
public class TimerTrace extends ClassVisitor implements Opcodes {
private String owner;
private boolean isInterface;
private String methodName;
public TimerTrace(int i, ClassVisitor classVisitor, String methodName) {
super(i, classVisitor);
this.methodName = methodName;
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
owner = name;
isInterface = (access & ACC_INTERFACE) != 0;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
//When the specified methodname is matched, bytecode is modified
if (!isInterface && mv != null && name.equals(methodName)) {
// System.out.println(" package.className:methodName()")
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.visitLdcInsn(" " + owner.replace("/", ".")
+ ":" + 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(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
//Methods code block time consumption statistics and printing
TimerMethodVisitor at = new TimerMethodVisitor(owner, access, name, descriptor, mv);
return at.getLocalVariablesSorter();
}
return mv;
}
public static void main(String[] args) throws IOException {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
TraceClassVisitor tv = new TraceClassVisitor(cw, new PrintWriter(System.out));
TimerTrace addFiled = new TimerTrace(Opcodes.ASM5, tv, "test");
ClassReader classReader = new ClassReader("demo.Test");
classReader.accept(addFiled, ClassReader.EXPAND_FRAMES);
File file = new File("out/production/asm-demo/demo/Test.class");
String parent = file.getParent();
File parent1 = new File(parent);
parent1.mkdirs();
file.createNewFile();
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(cw.toByteArray());
}
}
To count the time consumption of each line of code in the method, you only need to add the current time stamp before and after each line of code, and then subtract it.
So that’s what our code says.
public class TimerMethodVisitor extends MethodVisitor implements Opcodes {
private int start;
private int end;
private int maxStack;
private String lineContent;
public boolean instance = false;
private LocalVariablesSorter localVariablesSorter;
private AnalyzerAdapter analyzerAdapter;
public TimerMethodVisitor(String owner, int access, String name, String descriptor, MethodVisitor methodVisitor) {
super(Opcodes.ASM5, methodVisitor);
this.analyzerAdapter = new AnalyzerAdapter(owner, access, name, descriptor, this);
localVariablesSorter = new LocalVariablesSorter(access, descriptor, this.analyzerAdapter);
}
public LocalVariablesSorter getLocalVariablesSorter() {
return localVariablesSorter;
}
/**
*After entering the method, execute first
*So we can define a starting timestamp here, and then create a local variable var_ End
* Long var_start = System.nanoTime();
* Long var_end;
*/
@Override
public void visitCode() {
mv.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
start = localVariablesSorter.newLocal(Type.LONG_TYPE);
mv.visitVarInsn(ASTORE, start);
end = localVariablesSorter.newLocal(Type.LONG_TYPE);
maxStack = 4;
}
/**
*Add the following code after each line of code
* var_end = System.nanoTime();
* System.out.println("[" + String.valueOf((var_end.doubleValue() - var_start.doubleValue()) / 1000000.0D) + "ms] " + "package.className:methodName() #lineNumber");
* var_start = var_end;
* @param lineNumber
* @param label
*/
@Override
public void visitLineNumber(int lineNumber, Label label) {
super.visitLineNumber(lineNumber, label);
if (instance) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
mv.visitVarInsn(ASTORE, end);
// System.out
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// new StringBuilder();
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "", "()V", false);
mv.visitLdcInsn(" -[");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(ALOAD, end);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "doubleValue", "()D", false);
mv.visitVarInsn(ALOAD, start);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "doubleValue", "()D", false);
mv.visitInsn(DSUB);
mv.visitLdcInsn(new Double(1000 * 1000));
mv.visitInsn(DDIV);
// String.valueOf((end - start)/1000000)
mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "valueOf", "(D)Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn("ms] ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
// .append("owner:methodName() #line")
mv.visitLdcInsn(this.lineContent + "#" + lineNumber);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
// stringBuilder.toString()
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
// println stringBuilder.toString()
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
// start = end
mv.visitVarInsn(ALOAD, end);
mv.visitVarInsn(ASTORE, start);
maxStack = Math.max(analyzerAdapter.stack.size() + 4, maxStack);
}
instance = true;
}
/**
*Splicing bytecode content
* @param opcode
* @param owner
* @param methodName
* @param descriptor
* @param isInterface
*/
@Override
public void visitMethodInsn(int opcode, String owner, String methodName, String descriptor, boolean isInterface) {
super.visitMethodInsn(opcode, owner, methodName, descriptor, isInterface);
if (!isInterface && opcode == Opcodes.INVOKESTATIC) {
this.lineContent = owner.replace("/", ".")
+ ":" + methodName + "() ";
}
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(Math.max(maxStack, this.maxStack), maxLocals);
}
}
If beginners can’t change bytecode. You can use the ASM plug-in provided by idea for reference.
pack
In this way, an executable agent jar is written and packaged
@echo off
attrib -s -h -r -a /s /d asm
rd /s /q asm
rd /q agent.jar
javac -XDignore.symbol.file=true -encoding utf-8 -d . ./java/asm/*.java
jar cvfm agent.jar ./java/asm/MANIFEST.MF asm
rd /s /q asm
exit
test
Run target program target.jar
java -jar target.jar
Printing Test.test Each node in the
java -jar agent.jar [pid] demo.Test#test
result
Printing Operator.handler Method each node takes time