教你用Java位元組碼做點有趣的事(二)之ASM

咖啡拿鐵發表於2018-07-22

0.寫在前面

本篇是本系列的第二篇,主要介紹什麼是ASM,以及如何使用ASM。 如果沒有閱讀之前的教你用Java位元組碼做點有趣的事,還請閱讀一下,因為需要上一章的部分需求。

1.什麼是ASM

在上節我們知道,通過javac編譯生成之後生成的是位元組碼,但是我們可能會有一些需求,比如需要AOP切面,事務的統一管理,有些重複的程式碼需要我們來回的敲,又或者我們需要生成自己的位元組碼來使用(fastjson就是這麼做的)。但是位元組碼如果我們直接操作,成本太大,並且效率也不高。這個時候你就需要一款利器,將位元組碼轉換成java語言,從而你就可以隨心所欲的操縱位元組碼。這些工具如ASM,例如Javaassit,BCEL等等,都可以用來操作位元組碼。 而這裡我要介紹的就是操作位元組碼的一把利器-ASM,ASM是一個java位元組碼操縱框架,它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進位制 class 檔案,也可以在類被載入入 Java 虛擬機器之前動態改變類行為。Java class 被儲存在嚴格格式定義的 .class檔案裡,這些類檔案擁有足夠的後設資料來解析類中的所有元素:類名稱、方法、屬性以及 Java 位元組碼(指令)。ASM從類檔案中讀入資訊後,能夠改變類行為,分析類資訊,甚至能夠根據使用者要求生成新類。 ASM的優點如下:

  • 有一個簡單的模組API,設計完善,使用方便。
  • 更新速度快,支援最新的Java版本
  • 小而快,非常可靠
  • 已經有很多著名的開源框架都在使用,例如cglib,spring,fastjson等等。
  • 源許可開放,幾乎允許任意使用。 asm的具體流程如下所示:

教你用Java位元組碼做點有趣的事(二)之ASM

2.ASM的簡單入門

在這個小標題我會簡單的介紹,如何去使用ASM。在這裡之前我希望你有idea編譯器,如果你有的話,可以去外掛庫裡面下載一個ASM Bytecode Outline。有了這個我們後面開發ASM將會感受到美滋滋,如魚得水。

ASM 庫?供了兩個用於生成和轉換已編譯類的API,一個是核心API,以基於事件的形式來表示類,另一個是樹API,以基於物件的形式來表示類。

  • 核心api,可以對比XML中解析的SAX,不需要把這個類的整個結構讀取進來,節約記憶體,但是程式設計難度較大。在採用基於事件的模型時,類是用一系列事件來表示的,每個事件表示類的一個元素,比如它的一個欄位、一個方法宣告、一條指令,等等。基於事件的API定義了一組可能事件,以及這些事件必須遵循的發生順序,還?供了一個類分析器,為每個被分析元素生成一個事件,還?供一個類寫入器,由這些事件的序列生成經過編譯的類。
  • 樹API,對比XML解析中的DOM,需要把整個類的結構讀取到記憶體中,消耗記憶體多,但是變成較為簡單

沒有asm jar包的同學需要引入下面的maven:

        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>5.0.4</version>
        </dependency>
複製程式碼

2.1核心api

這裡大家是否已經下好了那個外掛(ASM Bytecode Outline)呢?如果下載完畢,還記得我們上一節的那個例子嗎?

public class ByteCodeDemo {
    private static final String name = "xiaoming";

    private int age;

    public ByteCodeDemo(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }
    public void setAge(){
        this.age = age;
    }
    public static void main(String[] args) {
        ByteCodeDemo byteCodeDeomo = new ByteCodeDemo(12);
        System.out.println("name:" + name + "age:" + byteCodeDeomo.getAge());
    }
}
複製程式碼

在這個類中,右鍵點選下方

教你用Java位元組碼做點有趣的事(二)之ASM
然後右方會生成我們的如果用ASM生成這個類,那麼應該是哪些程式碼:

教你用Java位元組碼做點有趣的事(二)之ASM
複製旁邊的程式碼,你就能生成你的class的二進位制檔案。

如果你看不懂,沒關係,我這裡會慢慢的講。

2.1.1 核心類

在ASM的core API程式設計中有幾個關鍵類:

  • ClassReader:可以讀取編譯好的二進位制Class檔案
  • ClassWriter:用來重新構建編譯後的類,比如說修改類名、屬性以及方法,甚至可以生成新的類的位元組碼檔案。
  • ClassVisitor:用於生成和變轉已編譯類。在ClassVisitor定義了很多方法,例如:類上的註解,類的構造方法,類的欄位,類的方法,靜態程式碼塊訪問。用於我們去重寫,以便做一些類上的邏輯擴充套件。要注意的是ClassWriter繼承的是ClassVistor,這裡ClassWriter就可以邊訪問邊寫入。
  • 上面說了ClassVisitor,這裡再說說其他的Visitor,ASM core api裡的程式碼是根據位元組碼從上到下依次生成,可以看見裡面還有一些其他的Visitor,比如MethodVisitor,用於訪問Method,如果重寫其部分方法可以對方法進行修改。FieldVisitor,用於訪問類的變數,常量,如果重寫其部分方法可以進行修改。AnnotationVisitor,用於訪問的註解。

visitor是我們擴充套件我們類自己碼的關鍵

2.2 coreApi如何編寫程式碼

 public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("java/java8/ByteCodeDemo.class");
        ClassReader classReader = new ClassReader(fileInputStream);
        ClassWriter cw = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
        //Java8選擇ASM5,
        ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, cw) {
            @Override
            public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
                System.out.println("field:" + name);
                return super.visitField(access, name, desc, signature, value);
            }

            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                System.out.println("方法" + name);
                return super.visitMethod(access, name, desc, signature, exceptions);
            }
        };
        //忽略除錯資訊
        classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);

    }
複製程式碼

我們下面輸出:

field:name
field:age
方法<init>
方法getAge
方法setAge
方法main
複製程式碼

可以看到我們已經通過visitField和visitMethod,進行對每個field和每個Method的名字都進行了輸出,其中方法包括了編譯器幫我們建立的構造方法和我們自定義的三個方法。

3.樹形API

由於後面的程式碼都會通過coreApi來做,這裡樹形API簡單用例子說明一下:

public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("/Users/lizhao/Documents/RPC/test/src/main/java/java8/ByteCodeDemo.class");
        ClassReader classReader = new ClassReader(fileInputStream);
        ClassWriter cw = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
        //忽略除錯資訊
        ClassNode classNode = new ClassNode(org.objectweb.asm.Opcodes.ASM5);


        classReader.accept(classNode, ClassReader.SKIP_DEBUG);
        for (MethodNode methodNode:classNode.methods) {
            System.out.println(methodNode.name);
        }
        classNode.accept(cw);
    }
複製程式碼

輸出如下:

<init>
getAge
setAge
main
複製程式碼

4.最後

本文簡單介紹了ASM的介紹,以及簡單的使用,下一章會介紹如何去用ASM做上一篇介紹的小工具,以及java的instrument機制。 同時想獲取ASM更多高階用法可以關注我的公眾號回覆asm即可獲取。

由於水平不足,如有錯誤還請批評與指正!

為了方便大家學習交流,建了個qq java後端交流群:837321192,裡面有我收藏的百G學習視訊(涵蓋面試,架構等等),也有很多面試資料,可以加入進來一起交流。

如果大家覺得這篇文章對你有幫助,或者想提前獲取後續章節文章,或者你有什麼疑問想提供1v1免費vip服務,都可以關注我的公眾號,關注即可免費領取上百G最新java學習資料視訊,以及最新面試資料,你的關注和轉發是對我最大的支援,O(∩_∩)O:

教你用Java位元組碼做點有趣的事(二)之ASM

相關文章