Java Dynamic bytecode technology is used to implement the trace function of Arthas.

Time:2020-11-24

reference material

ASM series detailed tutorial

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

attribute
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.

Java Dynamic bytecode technology is used to implement the trace function of Arthas.

 

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

Java Dynamic bytecode technology is used to implement the trace function of Arthas.

 

Printing Operator.handler Method each node takes time

Java Dynamic bytecode technology is used to implement the trace function of Arthas.

Recommended Today

Summary of recent use of gin

Recently, a new project is developed by using gin. Some problems are encountered in the process. To sum up, as a note, I hope it can help you. Cross domain problems Middleware: func Cors() gin.HandlerFunc { return func(c *gin.Context) { //Here you can use * or the domain name you specify c.Header(“Access-Control-Allow-Origin”, “*”) //Allow header […]