1. 程式人生 > >通過 ASM 庫生成和修改 class 檔案

通過 ASM 庫生成和修改 class 檔案

在 JVM中 Class 檔案分析 主要詳細講解了Class檔案的格式,並且在上一篇文章中做了總結。 眾所周知,JVM 在執行時, 載入並執行class檔案, 這個class檔案基本上都是由我們所寫的java原始檔通過 javac 編譯而得到的。 但是, 我們有時候會遇到這種情況:在前期(編寫程式時)不知道要寫什麼類,只有到執行時,才能根據當時的程式執行狀態知道要使用什麼類。 舉一個常見的例子就是 JDK 中的動態代理。這個代理能夠使用一套API代理所有的符合要求的類, 那麼這個代理就不可能在 JDK 編寫的時候寫出來,因為當時還不知道使用者要代理什麼類。 

當遇到上述情況時, 就要考慮這種機制:在執行時動態生成class檔案。 也就是說, 這個 class 檔案已經不是由你的 Java 原始碼編譯而來,而是由程式動態生成。 能夠做這件事的,有JDK中的動態代理API, 還有一個叫做 cglib 的開源庫。 這兩個庫都是偏重於動態代理的, 也就是以動態生成 class 的方式來支援代理的動態建立。 除此之外, 還有一個叫做 ASM 的庫, 能夠直接生成class檔案,它的 api 對於動態代理的 API 來說更加原生, 每個api都和 class 檔案格式中的特定部分相吻合, 也就是說, 如果對 class 檔案的格式比較熟練, 使用這套 API 就會相對簡單。 下面我們通過一個例項來講解 ASM 的使用, 並且在使用的過程中, 會對應 class 檔案中的各個部分來說明。

ASM 庫的介紹和使用

ASM 庫是一款基於 Java 位元組碼層面的程式碼分析和修改工具,那 ASM 和訪問者模式有什麼關係呢?訪問者模式主要用於修改和操作一些資料結構比較穩定的資料,通過前面的學習,我們知道 .class 檔案的結構是固定的,主要有常量池、欄位表、方法表、屬性表等內容,通過使用訪問者模式在掃描 .class 檔案中各個表的內容時,就可以修改這些內容了。在學習 ASM 之前,可以通過深入淺出訪問者模式 這篇文章學習一下訪問者模式。 

ASM 可以直接生產二進位制的 .class 檔案,也可以在類被載入入 JVM 之前動態修改類行為。下文將通過兩個例子,分別介紹如何生成一個 class 檔案和修改 Java 類中方法的位元組碼。

在剛開始使用的時候,可能對位元組碼的執行不是很清楚,使用 ASM 會比較困難,ASM 官方也提供了一個幫助工具 ASMifier,我們可以先寫出目的碼,然後通過 javac 編譯成 .class 檔案,然後通過 ASMifier 分析此 .class 檔案就可以得到需要插入的程式碼對應的 ASM 程式碼了。

ASM 生成 class 檔案

下面簡單看一個 java 類:

package work;

public class Example {public static void main(String[] var0) {
        System.out.println("createExampleClass");
    }
}

這個 Example 類很簡單,只有簡單的包名,加上一個靜態 main 方法,列印輸出 createExampleClass 。

現在問題來了,你如何生成這個 Example.java 的 class 檔案,不能在開發時通過上面的原始碼來編譯成, 而是要動態生成。

下面開始介紹如何使用 ASM 動態生成上述原始碼對應的位元組碼。

程式碼示例

public class Main extends ClassLoader {
    // 此處記得替換成自己的檔案地址
    public static final String PATH = "/Users/xxx/IdeaProjects/untitled/src/work/";

    public static void main(String[] args) {
        createExampleClass();
    }

    private static void createExampleClass() {
        ClassWriter cw = new ClassWriter(0);
        // 定義一個叫做Example的類,並且這個類是在 work 目錄下面
        cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "work/Example", null, "java/lang/Object", null);
        // 生成預設的構造方法
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        // 生成構造方法的位元組碼指令
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 1);
        // 建構函式訪問結束
        mv.visitEnd();

        // 生成main方法中的位元組碼指令
        mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
        mv.visitCode();
        // 獲取該方法
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        // 載入字串引數
        mv.visitLdcInsn("createExampleClass");
        // 呼叫該方法
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(2, 1);
        mv.visitEnd();

        // 獲取生成的class檔案對應的二進位制流
        byte[] code = cw.toByteArray();

        // 將二進位制流寫到本地磁碟上
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(PATH + "Example.class");
            fos.write(code);
            System.out.println(fos.getFD());
            fos.close();
        } catch (Exception e) {
            System.out.print(" FileOutputStream error " + e.getMessage());
            e.printStackTrace();
        }
        loadclass("Example.class", "work.Example");
    }

    private static void loadclass(String className, String packageNamePath) {
        //通過反射呼叫main方法
        MyClassLoader myClassLoader = new MyClassLoader(PATH + className);
        // 類的全稱,對應包名
        try {
            // 載入class檔案
            Class<?> Log = myClassLoader.loadClass(packageNamePath);
            System.out.println("類載入器是:" + Log.getClassLoader());
            // 利用反射獲取main方法
            Method method = Log.getDeclaredMethod("main", String[].class);
            String[] arg = {"ad"};
            method.invoke(null, (Object) arg);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

為了證明表示我們生成的 class 可以正常呼叫,還需要將其載入,然後通過反射呼叫該類的方法,這樣才能說明生成的 class 檔案是沒有問題並且可執行的。

下面是自定義的一個 class 載入類:

public class MyClassLoader extends ClassLoader {
    // 指定路徑
    private String path;

    public MyClassLoader(String classPath) {
        path = classPath;
    }

    /**
     * 重寫findClass方法
     *
     * @param name 是我們這個類的全路徑
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class log = null;
        // 獲取該class檔案位元組碼陣列
        byte[] classData = getData();

        if (classData != null) {
            // 將class的位元組碼陣列轉換成Class類的例項
            log = defineClass(name, classData, 0, classData.length);
        }
        return log;
    }

    /**
     * 將class檔案轉化為位元組碼陣列
     *
     * @return
     */
    private byte[] getData() {

        File file = new File(path);
        if (file.exists()) {
            FileInputStream in = null;
            ByteArrayOutputStream out = null;
            try {
                in = new FileInputStream(file);
                out = new ByteArrayOutputStream();

                byte[] buffer = new byte[1024];
                int size = 0;
                while ((size = in.read(buffer)) != -1) {
                    out.write(buffer, 0, size);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return out.toByteArray();
        } else {
            return null;
        }
    }
}

程式碼詳解

下面詳細介紹生成class的過程:

首先定義一個類

相關程式碼片段如下:
 ClassWriter cw = new ClassWriter(0);
 // 定義一個叫做Example的類,並且這個類是在 work 目錄下面
 cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "work/Example", null, "java/lang/Object", null);

ClassWriter 類是 ASM 中的核心 API , 用於生成一個類的位元組碼。 ClassWriter 的 visit 方法定義一個類。 

  • 第一個引數 V1_8 是生成的 class 的版本號, 對應class檔案中的主版本號和次版本號, 即 minor_version 和 major_version 。 

  • 第二個引數ACC_PUBLIC表示該類的訪問標識。這是一個public的類。 對應class檔案中的access_flags 。

  • 第三個引數是生成的類的類名。 需要注意,這裡是類的全限定名。 如果生成的class帶有包名, 如com.jg.xxx.Example, 那麼這裡傳入的引數必須是com/jg/xxx/Example  。對應 class 檔案中的 this_class  。

  • 第四個引數是和泛型相關的, 這裡我們不關新, 傳入null表示這不是一個泛型類。這個引數對應class檔案中的Signature屬性(attribute) 。

  • 第五個引數是當前類的父類的全限定名。 該類直接繼承Object。 這個引數對應class檔案中的super_class 。 

  • 第六個引數是 String[] 型別的, 傳入當前要生成的類的直接實現的介面。 這裡這個類沒實現任何介面, 所以傳入null 。 這個引數對應class檔案中的interfaces 。 

定義預設構造方法, 並生成預設構造方法的位元組碼指令  

相關程式碼片段如下:
        // 生成預設的構造方法
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
        // 生成構造方法的位元組碼指令
        mv.visitVarInsn(ALOAD, 0);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        mv.visitInsn(RETURN);
        mv.visitMaxs(1, 1);
        // 建構函式訪問結束
        mv.visitEnd();

使用上面建立的 ClassWriter 物件, 呼叫該物件的 visitMethod 方法, 得到一個 MethodVisitor 物件, 這個物件定義一個方法。 對應 class 檔案中的一個 method_info 。 

  • 第一個引數是 ACC_PUBLIC , 指定要生成的方法的訪問標誌。 這個引數對應 method_info 中的 access_flags 。 

  • 第二個引數是方法的方法名。 對於構造方法來說, 方法名為 <init> 。 這個引數對應 method_info 中的 name_index , name_index 引用常量池中的方法名字串。 

  • 第三個引數是方法描述符, 在這裡要生成的構造方法無引數, 無返回值, 所以方法描述符為 ()V  。 這個引數對應 method_info 中的descriptor_index 。 

  • 第四個引數是和泛型相關的, 這裡傳入null表示該方法不是泛型方法。這個引數對應 method_info 中的 Signature 屬性。

  • 第五個引數指定方法宣告可能丟擲的異常。 這裡無異常宣告丟擲, 傳入 null 。 這個引數對應 method_info 中的 Exceptions 屬性。

接下來呼叫 MethodVisitor 中的多個方法, 生成當前構造方法的位元組碼。 對應 method_info 中的 Code 屬性。

  1. 呼叫 visitVarInsn 方法,生成 aload 指令, 將第 0 個本地變數(也就是 this)壓入運算元棧。

  2. 呼叫 visitMethodInsn方法, 生成 invokespecial 指令, 呼叫父類(也就是 Object)的構造方法。

  3. 呼叫 visitInsn 方法,生成 return 指令, 方法返回。 

  4. 呼叫 visitMaxs 方法, 指定當前要生成的方法的最大區域性變數和最大運算元棧。 對應 Code 屬性中的 max_stack 和 max_locals 。 

  5. 最後呼叫 visitEnd 方法, 表示當前要生成的構造方法已經建立完成。 

定義main方法, 並生成main方法中的位元組碼指令

這裡與建構函式一樣,就不多說了。

生成class資料, 儲存到磁碟中, 載入class資料

 // 獲取生成的class檔案對應的二進位制流
        byte[] code = cw.toByteArray();

        // 將二進位制流寫到本地磁碟上
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(PATH + "Example.class");
            fos.write(code);
            fos.close();
        } catch (Exception e) {
            System.out.print(" FileOutputStream error " + e.getMessage());
            e.printStackTrace();
        }
        loadclass("Example.class", "work.Example");

這段程式碼執行完, 可以看到控制檯有以下輸出:

生成 ASM 程式碼

那麼還有個問題是前面的 ASM 程式碼是如何生成的呢?

還是以前文提到的 EXample.java 為例:

javac Example.java  // 生成 Example class 檔案
java -classpath asm-all-6.0_ALPHA.jar org.objectweb.asm.util.ASMifier Example.class  // 利用 ASMifier 將class 檔案轉為 asm 程式碼

在 Terminal 視窗中輸入這兩個命令,就可以得到下面的 asm 程式碼:

import java.util.*;
import org.objectweb.asm.*;
public class ExampleDump implements Opcodes {

public static byte[] dump () throws Exception {

ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;

cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "Example", null, "java/lang/Object", null);

{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("createExampleClass");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
}
cw.visitEnd();

return cw.toByteArray();
}
}

可以看到輸出結果與前面的生成的 class 檔案的程式碼是一樣的。

到這裡,相信你對 ASM 的使用已經有了初步的瞭解了,當然可能不是很熟悉,但是多寫寫練練掌握格式就好多了。

利用 ASM 修改方法

下面介紹如何修改一個 class 檔案的方法。

還是在原來的程式碼基礎上,Main 類下面新增一個方法 modifyMethod 方法,具體程式碼如下:

private static void modifyMethod() {
        byte[] code = null;
        try {
            // 需要注意把 . 變成 /, 比如 com.example.a.class 變成 com/example/a.class
            InputStream inputStream = new FileInputStream(PATH + "Example.class");
            ClassReader reader = new ClassReader(inputStream);                               // 1. 建立 ClassReader 讀入 .class 檔案到記憶體中
            ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);                 // 2. 建立 ClassWriter 物件,將操作之後的位元組碼的位元組陣列回寫
            ClassVisitor change = new ChangeVisitor(writer);                                        // 3. 建立自定義的 ClassVisitor 物件
            reader.accept(change, ClassReader.EXPAND_FRAMES);
            code = writer.toByteArray();
            System.out.println(code);
            FileOutputStream fos = new FileOutputStream(PATH + "Example.class");
            fos.write(code);
            fos.close();
        } catch (Exception e) {
            System.out.println("FileInputStream " + e.getMessage());
            e.printStackTrace();
        }
        try {
            if (code != null) {
                System.out.println(code);
                FileOutputStream fos = new FileOutputStream(PATH + "Example.class");
                fos.write(code);
                fos.close();
            }
        } catch (Exception e) {
            System.out.println("FileOutputStream ");
            e.printStackTrace();
        }
        loadclass("Example.class", "work.Example");
    }

新建一個 adapter,繼承自 AdviceAdapter,AdviceAdapter 本質也是一個 MethodVisitor,但是裡面對很多對方法的操作邏輯進行了封裝,使得我們不用關心 ASM 內部的訪問邏輯,只需要在對應的方法下面新增程式碼邏輯即可。

public class ChangeAdapter extends AdviceAdapter {
    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();
        Label l0 = new Label();
        Label l1 = new Label();
        Label l2 = new Label();
        mv.visitTryCatchBlock(l0, l1, l2, "java/lang/InterruptedException");
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        // 把當前的時間戳存起來
        mv.visitVarInsn(LSTORE, 1);
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("ChangeAdapter onMethodEnter ");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        mv.visitLabel(l0);
        mv.visitLdcInsn(new Long(100L));
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);
        mv.visitLabel(l1);
        Label l3 = new Label();
        mv.visitJumpInsn(GOTO, l3);
        mv.visitLabel(l2);
        mv.visitFrame(Opcodes.F_FULL, 2, new Object[] {"[Ljava/lang/String;", Opcodes.LONG}, 1, new Object[] {"java/lang/InterruptedException"});
        mv.visitVarInsn(ASTORE, 3);
        mv.visitVarInsn(ALOAD, 3);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/InterruptedException", "printStackTrace", "()V", false);
        mv.visitLabel(l3);
        mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        // 把當前的時間戳存起來
        mv.visitVarInsn(LSTORE, 3);
    }

    @Override
    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode);
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        // 把之前儲存的時間戳取出來
        mv.visitVarInsn(LLOAD, 3);
        mv.visitVarInsn(LLOAD, 1);
        mv.visitInsn(LSUB);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);
    }

    @Override
    public void visitMaxs(int i, int i1) {
        super.visitMaxs(i, i1);

    }

 在 adapter 中,有兩個非常重要的方法:

  • onMethodEnter:表示正在進入一個方法,在執行方法裡的內容前會呼叫。因此,此處是對一個方法新增相關處理邏輯的很好的辦法。

  • onMethodExit:表示正在退出一個方法,在執行 return 之前。如果一個方法存在返回值,只能再該方法新增靜態方法。

 上面的程式碼是為了計算某個方法的耗時,我們先是在方法開始前記錄了當前的時間戳,同時為了避免程式執行過快,還讓該執行緒睡了100ms。在方法結束前,將之前的時間戳取出來,同時獲取當前的時間戳,兩者相減,就是方法執行耗時。
public 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);
        System.out.print(name);
        if (name.equals("main")) {
            return new ChangeAdapter(Opcodes.ASM4, methodVisitor, access, name, desc);
        }
        return methodVisitor;
    }
}

 ChangeVisitor 主要就是對 ASM 訪問 class 檔案方法的時候,做個攔截。如果發現方法名是 main,就讓其走前面寫好的 ChangeAdapter,這樣,我們就可以改寫 class 檔案的方法了。

 執行結果

 可以看到輸出結果,是 100 ms,成功的對 main 方法的耗時進行了計算。

如果方法帶有返回值

前面修改的 main 是沒有返回值的,那麼如果存在返回值?這麼寫還合適嗎?

如果你添加了非靜態方法的呼叫,去看生成的 class 檔案也許可能是對的,但是在呼叫的時候就會報錯。示例如下:

    protected void onMethodExit(int opcode) {
        mv.visitVarInsn(LLOAD, longT);
        mv.visitInsn(LSUB);
        mv.visitVarInsn(LSTORE, longT);
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitLdcInsn("work2 createExampleClass");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        mv.visitVarInsn(LLOAD, longT);
    }

這裡是呼叫了一些非靜態方法,接下去看生成的 class 檔案:

 從class 檔案來看,生成的 class 檔案是沒有問題的,結果在反射呼叫的時候報了異常:

通過  javap -c Example.class 將反編譯結果輸出如下:

$ javap -c Example.class
public class work2.Example {
  public work2.Example();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public long computer();
    Code:
       0: ldc2_w        #29                 // long 32423l
       3: lstore_1
       4: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #18                 // String work2 createExampleClass
       9: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: invokestatic  #27                 // Method java/lang/System.currentTimeMillis:()J
      15: lstore_2
      16: invokestatic  #27                 // Method java/lang/System.currentTimeMillis:()J
      19: lstore        4
      21: lload         4
      23: lload_2
      24: lsub
      25: lstore        6
      27: lload         6
      29: lload_1
      30: lsub
      31: lstore_1
      32: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
      35: ldc           #18                 // String work2 createExampleClass
      37: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      40: lload_1
      41: lreturn
}

下面的是修改前的帶有返回值的反編譯結果:

$ javap  -c Example.class
public class work2.Example {
  public work2.Example();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

  public long computer();
    Code:
       0: ldc2_w        #29                 // long 32423l
       3: lstore_1
       4: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #18                 // String work2 createExampleClass
       9: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: invokestatic  #27                 // Method java/lang/System.currentTimeMillis:()J
      15: lstore_2
      16: invokestatic  #27                 // Method java/lang/System.currentTimeMillis:()J
      19: lstore        4
      21: lload         4
      23: lload_2
      24: lsub
      25: lstore        6
      27: lload         6
      29: lreturn
}

可以發現 27 行前面的程式碼都是一樣的,27 後面我們嘗試修改 class 檔案,同時替換返回值,但是最終還是失敗了。這裡原因我沒有去尋找,應該就是我們的修改導致堆疊資訊存在變化,從而導致校驗失敗。

如果我們實在需要對帶有返回值的返回值進行修改,可以參考下面的例項,使用靜態方法:

    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode);
        mv.visitLdcInsn("main");
        mv.visitMethodInsn(INVOKESTATIC, "work2/Main", "test", "(Ljava/lang/String;)V", false);
        mv.visitLdcInsn("ssss");
        mv.visitMethodInsn(INVOKESTATIC, "work2/Main", "test", "(Ljava/lang/String;)V", false);
    }

可以從 INVOKESTATIC 關鍵字看出,這些都是靜態方法。

 

到這裡,關於 ASM 使用說明到這裡就結束了。 

原始碼已上傳到 CSDN : ASM-demo.zip 。

 

參考文章:

從 Java 位元組碼到 ASM 實踐

Class檔案格式實戰:使用ASM動態生成class文