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 Hello {
    public static void main(String[] args) {
        show();
    }

    public static void show(){
        System.out.println("Hello World");
    }
}
複製代碼
上圖爲一個 Hello 類,要對 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 插件!!!
    建議安裝 ASM Bytecode Viewer 插件!!!
    建議安裝 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 測試效果

編寫測試類 運行java

public class Demo {
    public static void main(String[] args) throws IOException {
        ClassReader cr = new ClassReader(Hello.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/Hello.class");
        fos.write(code);
        fos.close();
    }
}
複製代碼

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

image.png

image.png

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

image.png

image.png

用途

能夠用於無痕埋點,打印日誌,以及性能監控等
複製代碼

TIPS:

Githubapp

相關文章
相關標籤/搜索