ASM簡單入門筆記

1.前言

前幾天,在Q羣裏有個大佬,展示了下 Android 做無痕埋點,覺得挺厲害的
問了下使用的是 AspectJ, 網上搜了下資料 ASM 比 AspectJ 更靈活,更輕量
剛好趁着五一假期系統的學習下

2. 介紹

ASM 是一款輕量級的Java字節碼操作倉庫

3. 前期準備

3.1 簡單的asm 方面的知識

ASM 主要有幾個類需要了解 而且需要對 Java字節碼 比較熟悉

ClassReader
    字節碼的讀取與分析引擎。它採用類似SAX的事件讀取機制,每當有事件發生時,調用註冊的ClassVisitor、AnnotationVisitor、FieldVisitor、MethodVisitor做相應的處理。

ClassVisitor
    定義在讀取Class字節碼時會觸發的事件,如類頭解析完成、註解解析、字段解析、方法解析等

AnnotationVisitor
    定義在解析註解時會觸發的事件,如解析到一個基本值類型的註解、enum值類型的註解、Array值類型的註解、註解值類型的註解等

FieldVisitor
    定義在解析字段時觸發的事件,如解析到字段上的註解、解析到字段相關的屬性等
    
MethodVisitor
    定義在解析方法時觸發的事件,如方法上的註解、屬性、代碼等。

ClassWriter
    它實現了ClassVisitor接口,用於拼接字節碼。

3.2 開發工具準備

idea / Android studio 
ASM Bytecode Viewer(對 Java字節碼 不熟悉的話必備)

4 實戰

4.1 要實現的效果

class User {
    public static void main(String[] args) {
        show();
    }

    public static void show(){
        System.out.println("Hello World");
    }
}
上圖爲一個 User類,要對 show() 方法的耗時進行計算並打印

4.2 編寫 ASM 邏輯

4.2.1 編寫 ClassVisitor

解析類的監聽器,解析Class字節碼時會觸發內部的方法
public class TestClassVisitor extends ClassVisitor {
    public TestClassVisitor(final ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        if (cv != null) {
            cv.visit(version, access, name, signature, superName, interfaces);
        }
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        //如果methodName是show,則返回我們自定義的TestMethodVisitor
        if ("show".equals(name)) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
            return new TestMethodVisitor(mv);
        }
        if (cv != null) {
            return cv.visitMethod(access, name, desc, signature, exceptions);
        }
        return null;
    }
}

4.2.2 編寫 MethodVisitor

解析方法的監聽器,解析Method時會觸發內部的方法
編寫前若對 Java字節碼 不熟悉 建議安裝 ASM Bytecode Viewer 插件

先新建一個類 編寫要注入的代碼,然後用插件查看

image.png

public class TestMethodVisitor extends MethodVisitor implements Opcodes {
    public TestMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }

    @Override
    public void visitCode() {
        //方法體內開始時調用
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        mv.visitVarInsn(LSTORE, 0);
        super.visitCode();
    }
    @Override
    public void visitInsn(int opcode) {
        //每執行一個指令都會調用
        if (opcode == Opcodes.RETURN) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LLOAD, 0);
            mv.visitInsn(LSUB);
            mv.visitVarInsn(LSTORE, 2);
            Label l3 = new Label();
            mv.visitLabel(l3);
            mv.visitLineNumber(11, l3);
            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", "<init>", "()V", false);
            mv.visitLdcInsn("== method cost time = ");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(LLOAD, 2);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn(" ==");
            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);

        }
        super.visitInsn(opcode);
    }
}

4.3 測試效果

編寫測試類 運行

public class Demo {
    public static void main(String[] args) throws IOException {
        ClassReader cr = new ClassReader(User.class.getName());
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        ClassVisitor cv = new TestClassVisitor(cw);
        cr.accept(cv, Opcodes.ASM5);
        // 獲取生成的class文件對應的二進制流
        byte[] code = cw.toByteArray();
        //將二進制流寫到out/下
        FileOutputStream fos = new FileOutputStream("out/User.class");
        fos.write(code);
        fos.close();
    }
}

原User 類生成的 .class 文件,以及輸出效果

image.png

image.png

字節碼修改後的 User 類的 .class 文件以及輸出效果

image.png

image.png

用途

可以用於無痕埋點,打印日誌,以及性能監控等

TIPS:

Github地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章