通過 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文件


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM