使用ASM框架實現統計函式執行時間

一個不願透漏真實姓名的碼農發表於2020-11-18

定義

ASM 是一個 Java 位元組碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進位制 class 檔案,也可以在類被載入入 Java 虛擬機器之前動態改變類行為。Java class 被儲存在嚴格格式定義的 .class 檔案裡,這些類檔案擁有足夠的後設資料來解析類中的所有元素:類名稱、方法、屬性以及 Java 位元組碼(指令)。ASM 從類檔案中讀入資訊後,能夠改變類行為,分析類資訊,甚至能夠根據使用者要求生成新類。

實現

現在使用ASM框架,在不修改原始碼的情況下實現統計函式執行時間

定義一個方法,在方法裡使用Thread.sleep() 模擬函式呼叫的耗時

public class Test {

    public void m() {
        System.out.println("方法執行了...");
        try {
            Thread.sleep((long) ((Math.random() + 1) * 1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

需要加入的統計功能,TimeStat類定義了兩個方法,start()方法表示函式呼叫開始,end()方法表示函式呼叫結束,並記錄函式的執行耗時

public class TimeStat {
    static ThreadLocal<Long> t = new ThreadLocal<Long>();

    public static void start() {
        t.set(System.currentTimeMillis());
    }

    public static void end() {

        System.out.println(Thread.currentThread().getStackTrace()[2] + " spend:" + (System.currentTimeMillis() - t.get()));
    }
}

將TimeStat的start、end方法織入Test.m()

import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassWriter;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class TimeStatWeaveGenerator {

    public static void main(String[] args) throws IOException {
        String className = Test.class.getName();
        ClassReader cr = new ClassReader(className);
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
        TimeStatClassAdapter classAdapter = new TimeStatClassAdapter(cw);
        cr.accept(classAdapter, ClassReader.SKIP_DEBUG);
        byte[] bytes = cw.toByteArray();
        //file地址為本地的class檔案路徑
        File file = new File("G:\\project\\JVM\\target\\classes/" + className.replaceAll("\\.", "/") + ".class");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        fileOutputStream.write(bytes);
        fileOutputStream.close();

    }
}

TimeStatClassAdapter類完成具體的位元組碼修改工作,重寫ClassVisitor類,覆蓋visitMethod方法,對m()方法進行修改,判斷是否是m()方法,如果是則進行方法位元組碼的調整,並將這個工作委託給TimeStatMethodAdapter完成

import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;

public class TimeStatClassAdapter extends ClassVisitor {

    public TimeStatClassAdapter(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
        MethodVisitor adapter = methodVisitor;
        if (methodVisitor != null) {
            if (name.equals("m")) {
                adapter = new TimeStatMethodAdapter(methodVisitor);
            }
        }
        return adapter;
    }
}

TimeStatClassAdapter visitCode()在方法的Code屬性被訪問時呼叫,在此時插入TimeStat.start()方法,表示方法的開始,覆蓋visitInsn()方法,當訪問xreturn指令 時,呼叫TimeStat.end()方法

import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;

import static jdk.internal.org.objectweb.asm.Opcodes.IRETURN;
import static jdk.internal.org.objectweb.asm.Opcodes.RETURN;

public class TimeStatMethodAdapter extends MethodVisitor {

    public TimeStatMethodAdapter(MethodVisitor methodVisitor) {
        super(Opcodes.ASM5, methodVisitor);
    }

    @Override
    public void visitCode() {
        //第二個引數為TimeStat類的全限定名
        visitMethodInsn(Opcodes.INVOKESTATIC, "xx/xx/xx/TimeStat", "start", "()V");
        super.visitCode();
    }

    @Override
    public void visitInsn(int opcode) {
        if ((opcode >= IRETURN && opcode <= RETURN)) {
            visitMethodInsn(Opcodes.INVOKESTATIC, "xx/xx/xx/TimeStat", "end", "()V");
        }
        mv.visitInsn(opcode);
    }
}

執行方法

public class RunASMMain {

    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        test.m();
    }
}

先執行TimeStatWeaveGenerator再執行RunASMMain,結果如下:

方法執行了...
xx.xx.xx.Test.m(Unknown Source) spend:1955

相關文章