ASM字節(jié)碼插樁詳解

1、ASM概述

  • ASM是一個功能比較齊全的java字節(jié)碼操作與分析框架掉弛,通過ASM框架症见,我們可以動態(tài)的生成類或者增強已有類的功能。
  • ASM可以直接生成二進制.class文件殃饿,也可以在類被加載入java虛擬機之前動態(tài)改變現(xiàn)有類的行為谋作。
  • java的二進制文件被存儲在嚴格格式定義的.class文件里,這些字節(jié)碼文件擁有足夠的元數(shù)據(jù)信息用來表示類中的所有元素乎芳,包括類名稱遵蚜、方法、屬性以及java字節(jié)碼指令奈惑。ASM從字節(jié)碼文件讀入這些信息后吭净,能夠改變類行為、分析類的信息肴甸,甚至還可以根據(jù)具體的要求生成新的類寂殉。
  • ASM 通過樹這種數(shù)據(jù)結(jié)構(gòu)來表示復雜的字節(jié)碼結(jié)構(gòu),因為需要處理字節(jié)碼結(jié)構(gòu)是固定的原在,所以可以利用Visitor(訪問者) 設(shè)計模式來對樹進行遍歷友扰,在遍歷過程中對字節(jié)碼進行修改。

2庶柿、Java 類文件概述

所謂 Java 類文件村怪,就是通常用 javac 編譯器產(chǎn)生的 .class 文件。這些文件具有嚴格定義的格式浮庐。Java 源文件經(jīng)過 javac 編譯器編譯之后甚负,將會生成對應(yīng)的二進制文件。

Java 類文件是 8 位字節(jié)的二進制流审残。數(shù)據(jù)項按順序存儲在 class 文件中梭域,相鄰的項之間沒有間隔,這使得 class 文件變得緊湊维苔,減少存儲空間碰辅。一個簡單的Hello World程序

public class HelloWorld { 
    public static void main(String[] args) { 
        System.out.println("Hello world"); 
    } 
}

經(jīng)過 javac 編譯后,得到的類文件HelloWorld.class介时,該文件中是由十六進制符號組成的没宾,這一段十六進制符號組成的長串是嚴格遵守 Java 虛擬機規(guī)范。用vim查看HelloWorld.class

vim HelloWorld.class

打開文件后輸入

:%!xxd

按回車即可看到如下一串串十六進制符號

HelloWorld.class文件構(gòu)成如下:


從上圖中可以看到沸柔,一個 Java 類文件大致可以歸為 10 個項:

  • Magic:該項存放了一個 Java 類文件的魔數(shù)(magic number)循衰,一個 Java 類文件的前 4 個字節(jié)被稱為它的魔數(shù)。每個正確的 Java 類文件都是以 0xCAFEBABE 開頭的褐澎,這樣保證了 Java 虛擬機能很輕松的分辨出 Java 文件和非 Java 文件会钝。
    有趣的是,魔數(shù)的固定值是Java之父James Gosling制定的,為CafeBabe(咖啡寶貝)迁酸,而Java的圖標為一杯咖啡先鱼。

  • Version:該項存放了 Java 類文件的版本信息

  • Constant Pool:常量池中存儲兩類常量:字面量與符號引用。字面量為文本字符串和代碼中聲明為Final的常量值奸鬓,符號引用如類和接口的全局限定名焙畔、字段的名稱和描述符、方法的名稱和描述符串远。

  • Access_flag:該項指明了該文件中定義的是類還是接口(一個 class 文件中只能有一個類或接口)宏多,同時還指明了類或接口的訪問標志,如 public澡罚,private, abstract 等信息伸但。

  • This Class:指向表示該類全限定名稱的字符串常量的指針。

  • Super Class:指向表示父類全限定名稱的字符串常量的指針留搔。

  • Interfaces:一個指針數(shù)組更胖,存放了該類或父類實現(xiàn)的所有接口名稱的字符串常量的指針。

  • Fields:該項對類或接口中聲明的字段進行了細致的描述催式。需要注意的是函喉,fields 列表中僅列出了本類或接口中的字段,并不包括從超類和父接口繼承而來的字段荣月。

  • Methods:該項對類或接口中聲明的方法進行了細致的描述。例如方法的名稱梳毙、參數(shù)和返回值類型等哺窄。需要注意的是,methods 列表里僅存放了本類或本接口中的方法账锹,并不包括從超類和父接口繼承而來的方法萌业。

  • Class attributes:該項存放了在該文件中類或接口所定義的屬性的基本信息。

3奸柬、ASM庫的結(jié)構(gòu)

  • Core:為其他包提供基礎(chǔ)的讀生年、寫、轉(zhuǎn)化Java字節(jié)碼和定義的API廓奕,并且可以生成Java字節(jié)碼和實現(xiàn)大部分字節(jié)碼的轉(zhuǎn)換抱婉。

  • Tree:提供了 Java 字節(jié)碼在內(nèi)存中的表現(xiàn)

  • Commons:提供了一些常用的簡化字節(jié)碼生成、轉(zhuǎn)換的類和適配器

  • Util:包含一些幫助類和簡單的字節(jié)碼修改類桌粉,有利于在開發(fā)或者測試中使用

  • XML:提供一個適配器將XML和SAX-comliant轉(zhuǎn)化成字節(jié)碼結(jié)構(gòu)蒸绩,可以允許使用XSLT去定義字節(jié)碼轉(zhuǎn)化

4、ASM Core API

  • ClassReader:這個類會將 .class 文件讀入到 ClassReader 中的字節(jié)數(shù)組中铃肯,它的 accept 方法接受一個 ClassVisitor 實現(xiàn)類患亿,并按照順序調(diào)用 ClassVisitor 中的方法

  • ClassVisitor:主要負責訪問類的成員信息。包括標記在類上的注解押逼、類的構(gòu)造方法步藕、類的字段惦界、類的方法、靜態(tài)代碼塊等

  • ClassWriter:ClassWriter 是一個 ClassVisitor 的子類咙冗,是和 ClassReader 對應(yīng)的類表锻,ClassReader 是將 .class 文件讀入到一個字節(jié)數(shù)組中,ClassWriter 是將修改后的類的字節(jié)碼內(nèi)容以字節(jié)數(shù)組的形式輸出乞娄。

  • AdviceAdapter:MethodVisitor 是一個抽象類瞬逊,當 ASM 的 ClassReader 讀取到 Method 時就轉(zhuǎn)入 MethodVisitor 接口處理。AdviceAdapter 是 MethodVisitor 的子類仪或,使用 AdviceAdapter 可以更方便的修改方法的字節(jié)碼确镊。AdviceAdapter其中幾個重要方法如下:
    void visitCode():表示 ASM 開始掃描這個方法
    void onMethodEnter():進入這個方法
    void onMethodExit():即將從這個方法出去
    void onVisitEnd():表示方法掃描完畢

我們來重點看下ClassVisitor
ClassVisitor類的API如下

image.png

4.1 visit

    /**
     * 可以拿到類的詳細信息
     *
     * @param version jdk的版本: 52 代表jdk版本 1.8;51 代表jdk版本 1.7
     * @param access 類的修飾符:ACC_PUBLIC范删、ACC_PRIVATE蕾域、ACC_PROTECTED、ACC_FINAL到旦、ACC_SUPER
     * @param name 類的名稱:以路徑的形式表示 com/joker/demo/TestClass
     * @param signature 泛型信息:未定義泛型旨巷,則該參數(shù)為null
     * @param superName 表示當前類所繼承的父類
     * @param interfaces 表示類所實現(xiàn)的接口列表
     */
    @Override
    void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces)
    }

類的修飾符
類的修飾符以“ACC_開頭”,可以作用到類級別上的修飾符主要有下面這些

修飾符 含義
ACC_PUBLIC public
ACC_PRIVATE private
ACC_PROTECTED protected
ACC_FINAL final
ACC_SUPER extends
ACC_INTERFACE 接口
ACC_ABSTRACT 抽象類
ACC_ANNOTATION 注解類型
ACC_ENUM 枚舉類型
ACC_DEPRECATED 標記了@Deprecated注解的類
ACC_SYNTHETIC javac生成

4.2 visitAnnotation

    /**
     * 當掃描器掃描到類注解聲明時進行調(diào)用
     *
     * @param desc 注解類型(簽名類型)
     * @param visible 注解是否可以在 JVM 中可見
     * @return
     */
    @Override
    AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        return super.visitAnnotation(desc, visible)
    }

4.3 visitField

/**
     * 當掃描器掃描到類中字段時進行調(diào)用
     *
     * @param access 修飾符
     * @param name 字段名
     * @param desc 字段類型
     * @param signature 泛型描述
     * @param value 默認值
     * @return
     */
    @Override
    FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        return super.visitField(access, name, desc, signature, value)
    }

4.4 visitMethod

    /**
     * 當掃描器掃描到類的方法時調(diào)用
     *
     * @param access 方法的修飾符
     * @param name 方法名
     * @param desc 方法簽名
     * @param signature 表示泛型相關(guān)的信息
     * @param exceptions 表示將會拋出的異常添忘,如果方法沒有拋出異常采呐,則參數(shù)為空
     * @return
     */
    @Override
    MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        return super.visitMethod(access, name, desc, signature, exceptions)
    }

方法的修飾符
可以作用到方法級別上的修飾符主要有下面這些

修飾符 含義
ACC_PUBLIC public
ACC_PRIVATE private
ACC_PROTECTED protected
ACC_STATIC static
ACC_FINAL final
ACC_SYNCHRONIZED 同步的
ACC_VARARGS 不定參數(shù)個數(shù)的方法
ACC_NATIVE native類型方法
ACC_ABSTRACT 抽象的方法
ACC_DEPRECATED 標記了@Deprecated注解的類
ACC_SYNTHETIC javac生成

方法的簽名格式
(參數(shù)列表)返回值類型

在ASM中不同的類型對應(yīng)不同的代碼,詳細的對應(yīng)關(guān)系如下表

代碼 類型
I int
B byte
C char
D double
F float
J long
S short
Z boolean
V void
[...; 數(shù)組
[[...; 二維數(shù)組
[[[...; 三維數(shù)組

方法參數(shù)列表對應(yīng)的方法簽名示例如下

參數(shù)列表 方法參數(shù)
String[] [Ljava/lang/String;
String[][] [[Ljava/lang/String;
int搁骑,String斧吐,String[] ILjava/lang/String;[Ljava/lang/String;
int,boolean仲器,long煤率,String[],double IZJ[Ljava/lang/String;D
Class<?>, String, Object...paramType Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/Object;
int[] [I

4.5 visitEnd

    /**
     * 當掃描器完成類掃描時才會調(diào)用
     */
    @Override
    void visitEnd() {
        super.visitEnd()
    }

5乏冀、ASM練手demo實現(xiàn)統(tǒng)計方法時長代碼插樁

5.1添加ASM依賴

implementation 'org.ow2.asm:asm-all:5.2'

5.2定義一個HelloWorld類

public class HelloWorld {

    public void sayHello() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

5.3通過javac命令執(zhí)行HelloWorld.java得到HelloWorld.class

目前桌面上已經(jīng)生成了HelloWorld.class字節(jié)碼文件

5.4新建一個ASMTest類蝶糯,從桌面讀取HelloWorld.class文件,通過ASM讀取HelloWorld.class文件辆沦,并將打印sayHello()方法調(diào)用時長的代碼插樁到sayHello()方法中昼捍,輸出新的字節(jié)碼文件OutputHelloWorld.class到桌面

public class ASMTest {

    public static void redefineHelloWorldClass() {
        try {
            InputStream inputStream = new FileInputStream("/Users/jokerwan/Desktop/HelloWorld.class");
            // 1. 創(chuàng)建 ClassReader 讀入 .class 文件到內(nèi)存中
            ClassReader reader = new ClassReader(inputStream);
            // 2. 創(chuàng)建 ClassWriter 對象,將操作之后的字節(jié)碼的字節(jié)數(shù)組回寫
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);
            // 3. 創(chuàng)建自定義的 ClassVisitor 對象
            ClassVisitor change = new ChangeVisitor(writer);
            // 4. 將 ClassVisitor 對象傳入 ClassReader 中
            reader.accept(change, ClassReader.EXPAND_FRAMES);

            System.out.println("Success!");
            // 獲取修改后的 class 文件對應(yīng)的字節(jié)數(shù)組
            byte[] code = writer.toByteArray();
            try {
                // 將二進制流寫到本地磁盤上
                FileOutputStream fos = new FileOutputStream("/Users/jokerwan/Desktop/OutputHelloWorld.class");
                fos.write(code);
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("Failure!");
        }
    }

    static class ChangeVisitor extends ClassVisitor {

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

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
            if (name.equals("<init>")) {
                return methodVisitor;
            }
            return new ChangeAdapter(Opcodes.ASM4, methodVisitor, access, name, desc);
        }
    }

    static class ChangeAdapter extends AdviceAdapter {
        private int startTimeId = -1;

        private String methodName = null;

        ChangeAdapter(int api, MethodVisitor mv, int access, String name, String desc) {
            super(api, mv, access, name, desc);
            methodName = name;
        }

        @Override
        protected void onMethodEnter() {
            super.onMethodEnter();
            startTimeId = newLocal(Type.LONG_TYPE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitIntInsn(LSTORE, startTimeId);
        }

        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);
            int durationId = newLocal(Type.LONG_TYPE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LLOAD, startTimeId);
            mv.visitInsn(LSUB);
            mv.visitVarInsn(LSTORE, durationId);
            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("The cost time of " + methodName + "() is ");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(LLOAD, durationId);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn(" ms");
            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);

        }
    }
}

5.5通過單元測試執(zhí)行ASMTest.redefineHelloWorldClass();

public class ExampleUnitTest {
    @Test
    public void testASM() {
        ASMTest.redefineHelloWorldClass();
    }
}

OutputHelloWorld.class已經(jīng)輸出到桌面

OutputHelloWorld.class拖到Android Studio中众辨,Android Studio會將字節(jié)碼文件反編譯為java文件端三,反編譯后的代碼如下

可以看到我們成功通過ASM將統(tǒng)計運行時長的代碼插入到sayHello()方法中。

demo代碼如下
https://github.com/isJoker/ASM_Demo

參考文章
https://asm.ow2.io/developer-guide.html#classreader
https://www.ibm.com/developerworks/cn/java/j-lo-asm30/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鹃彻,一起剝皮案震驚了整個濱河市郊闯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖团赁,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件育拨,死亡現(xiàn)場離奇詭異,居然都是意外死亡欢摄,警方通過查閱死者的電腦和手機熬丧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怀挠,“玉大人析蝴,你說我怎么就攤上這事÷塘埽” “怎么了闷畸?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長吞滞。 經(jīng)常有香客問我佑菩,道長,這世上最難降的妖魔是什么裁赠? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任殿漠,我火速辦了婚禮,結(jié)果婚禮上佩捞,老公的妹妹穿的比我還像新娘绞幌。我一直安慰自己,他們只是感情好失尖,可當我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布啊奄。 她就那樣靜靜地躺著,像睡著了一般掀潮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上琼富,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天仪吧,我揣著相機與錄音,去河邊找鬼鞠眉。 笑死薯鼠,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的械蹋。 我是一名探鬼主播出皇,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼哗戈!你這毒婦竟也來了郊艘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎纱注,沒想到半個月后畏浆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡狞贱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年刻获,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞎嬉。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡蝎毡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出氧枣,到底是詐尸還是另有隱情沐兵,我是刑警寧澤,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布挑胸,位于F島的核電站痒筒,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏茬贵。R本人自食惡果不足惜簿透,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望解藻。 院中可真熱鬧老充,春花似錦、人聲如沸螟左。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽胶背。三九已至巷嚣,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钳吟,已是汗流浹背廷粒。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留红且,地道東北人坝茎。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像暇番,于是被迫代替她去往敵國和親嗤放。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,585評論 2 359

推薦閱讀更多精彩內(nèi)容