HelloAsm(三)使用ASM動態生成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示例:HelloWorld

ASM的實現基於一套Java API, 所以我們首先得到ASM庫, 在這個我使用的是ASM 4.0的jar包 。 

首先以ASM中的HelloWorld實例來講解, 比如我們要生成以下代碼對應的class文件:

public class Example {

  public static void main (String[] args) {
    System.out.println("Hello world!");
}
但是這個class文件不能在開發時通過上面的源碼來編譯成, 而是要動態生成。 下面我們介紹如何使用ASM動態生成上述源碼對應的字節碼。

下面是代碼示例(該實例來自於ASM官方的sample):

import java.io.FileOutputStream;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class Helloworld extends ClassLoader implements Opcodes {

    public static void main(final String args[]) throws Exception {


  //定義一個叫做Example的類
  ClassWriter cw = new ClassWriter(0);
  cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);

  //生成默認的構造方法
  MethodVisitor mw = cw.visitMethod(ACC_PUBLIC,
    "<init>",
    "()V",
    null,
    null);

  //生成構造方法的字節碼指令
  mw.visitVarInsn(ALOAD, 0);
  mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
  mw.visitInsn(RETURN);
  mw.visitMaxs(1, 1);
  mw.visitEnd();

  //生成main方法
  mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
    "main",
    "([Ljava/lang/String;)V",
    null,
    null);

  //生成main方法中的字節碼指令
  mw.visitFieldInsn(GETSTATIC,
    "java/lang/System",
    "out",
    "Ljava/io/PrintStream;");

  mw.visitLdcInsn("Hello world!");
  mw.visitMethodInsn(INVOKEVIRTUAL,
    "java/io/PrintStream",
    "println",
    "(Ljava/lang/String;)V");
  mw.visitInsn(RETURN);
  mw.visitMaxs(2, 2);

  //字節碼生成完成
  mw.visitEnd();

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


  //將二進制流寫到本地磁盤上
  FileOutputStream fos = new FileOutputStream("Example.class");
  fos.write(code);
  fos.close();

  //直接將二進制流加載到內存中
  Helloworld loader = new Helloworld();
  Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);

  //通過反射調用main方法
  exampleClass.getMethods()[0].invoke(null, new Object[] { null });

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

1 首先定義一個類

相關代碼片段如下:

//定義一個叫做Example的類
        ClassWriter cw = new ClassWriter(0);
        cw.visit(V1_1, ACC_PUBLIC, "Example", null, "java/lang/Object", null);
ClassWriter類是ASM中的核心API , 用於生成一個類的字節碼。 ClassWriter的visit方法定義一個類。 

第一個參數V1_1是生成的class的版本號, 對應class文件中的主版本號和次版本號, 即 minor_version和 major_version 。 

第二個參數ACC_PUBLIC表示該類的訪問標識。這是一個public的類。 對應class文件中的 access_flags 。

第三個參數是生成的類的類名。 需要注意,這裏是類的全限定名。 如果生成的class帶有包名, 如com.jg.zhang.Example, 那麼這裏傳入的參數必須是com/jg/zhang/Example  。對應class文件中的 this_class  。

第四個參數是和泛型相關的, 這裏我們不關新, 傳入null表示這不是一個泛型類。這個參數對應class文件中的Signature屬性(attribute) 。

 

第五個參數是當前類的父類的全限定名。 該類直接繼承Object。 這個參數對應class文件中的 super_class 。 

第六個參數是String[]類型的, 傳入當前要生成的類的直接實現的接口。 這裏這個類沒實現任何接口, 所以傳入null 。 這個參數對應class文件中的 interfaces 。 

2 定義默認構造方法, 並生成默認構造方法的字節碼指令  

相關代碼片段如下:

//生成默認的構造方法
  MethodVisitor mw = cw.visitMethod(ACC_PUBLIC,
    "<init>",
    "()V",
    null,
    null);

  //生成構造方法的字節碼指令
  mw.visitVarInsn(ALOAD, 0);
  mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
  mw.visitInsn(RETURN);
  mw.visitMaxs(1, 1);
  mw.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 中的 S ignature屬性。

第五個參數指定方法聲明可能拋出的異常。 這裏無異常聲明拋出, 傳入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方法, 表示當前要生成的構造方法已經創建完成。 

3 定義main方法, 並生成main方法中的字節碼指令

對應的代碼片段如下:

mw = cw.visitMethod(ACC_PUBLIC + ACC_STATIC,
    "main",
    "([Ljava/lang/String;)V",
    null,
    null);

  //生成main方法中的字節碼指令
  mw.visitFieldInsn(GETSTATIC,
    "java/lang/System",
    "out",
    "Ljava/io/PrintStream;");

  mw.visitLdcInsn("Hello world!");
  mw.visitMethodInsn(INVOKEVIRTUAL,
    "java/io/PrintStream",
    "println",
    "(Ljava/lang/String;)V");
  mw.visitInsn(RETURN);
  mw.visitMaxs(2, 2);
  mw.visitEnd();
這個過程和上面的生成默認構造方法的過程是一致的。 讀者可對比上一步執行分析。

4 生成class數據, 保存到磁盤中, 加載class數據

對應代碼片段如下:

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


  //將二進制流寫到本地磁盤上
  FileOutputStream fos = new FileOutputStream("Example.class");
  fos.write(code);
  fos.close();

  //直接將二進制流加載到內存中
  Helloworld loader = new Helloworld();
  Class<?> exampleClass = loader.defineClass("Example", code, 0, code.length);

  //通過反射調用main方法
  exampleClass.getMethods()[0].invoke(null, new Object[] { null });

這段代碼首先獲取生成的class文件的字節流, 把它寫在本地磁盤的Example.class文件中。 然後加載class字節流, 並通過反射調用main方法。

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

Hello world!
然後在當前測試工程的根目錄下, 生成一個Example.class文件文件。

下面我們使用javap反編譯這個class文件:

javap -c -v -classpath . -private Example
輸出的完整信息如下:
Classfile /C:/Users/紀剛/Desktop/生成字節碼/AsmJavaTest/Example.class
  Last modified 2014-4-5; size 338 bytes
  MD5 checksum 281abde0e2012db8ad462279a1fbb6a4
public class Example
  minor version: 3
  major version: 45
  flags: ACC_PUBLIC
Constant pool:
  #1 = Utf8					Example
  #2 = Class				  #1				 //  Example
  #3 = Utf8					java/lang/Object
  #4 = Class				  #3				 //  java/lang/Object
  #5 = Utf8					<init>
  #6 = Utf8					()V
  #7 = NameAndType		  #5:#6			 //  "<init>":()V
  #8 = Methodref			 #4.#7			 //  java/lang/Object."<init>":()V
  #9 = Utf8					main
  #10 = Utf8					([Ljava/lang/String;)V
  #11 = Utf8					java/lang/System
  #12 = Class				  #11				//  java/lang/System
  #13 = Utf8					out
  #14 = Utf8					Ljava/io/PrintStream;
  #15 = NameAndType		  #13:#14		  //  out:Ljava/io/PrintStream;
  #16 = Fieldref			  #12.#15		  //  java/lang/System.out:Ljava/io/PrintStream;
  #17 = Utf8					Hello world!
  #18 = String				 #17				//  Hello world!
  #19 = Utf8					java/io/PrintStream
  #20 = Class				  #19				//  java/io/PrintStream
  #21 = Utf8					println
  #22 = Utf8					(Ljava/lang/String;)V
  #23 = NameAndType		  #21:#22		  //  println:(Ljava/lang/String;)V
  #24 = Methodref			 #20.#23		  //  java/io/PrintStream.println:(Ljava/lang/String;)V
  #25 = Utf8					Code
{
  public Example();
   flags: ACC_PUBLIC
   Code:
    stack=1, locals=1, args_size=1
      0: aload_0
      1: invokespecial #8						// Method java/lang/Object."<init>":()V
      4: return

  public static void main(java.lang.String[]);
   flags: ACC_PUBLIC, ACC_STATIC
   Code:
    stack=2, locals=2, args_size=1
      0: getstatic	  #16					  // Field java/lang/System.out:Ljava/io/PrintStream;
      3: ldc			  #18					  // String Hello world!
      5: invokevirtual #24					  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      8: return
}
正是一個標準的class格式的文件, 它和以下源碼是對應的:
public class Example {

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

只是, 上面的class文件不是由這段源代碼生成的, 而是使用ASM動態創建的。 

ASM示例二: 生成字段, 並給字段加註解

上面的HelloWorld示例演示瞭如何生成類和方法, 該示例演示如何生成字段, 並給字段加註解。 

public class BeanTest extends ClassLoader implements Opcodes {

  /*
   * 生成以下類的字節碼
   * 
   * public class Person {
   * 
   * 		@NotNull
   * 		public String name;
   * 
   * }
   */

  public static void main(String[] args) throws Exception {
    
    /********************************class***********************************************/

    // 創建一個ClassWriter, 以生成一個新的類

    ClassWriter cw = new ClassWriter(0);
    cw.visit(V1_6, ACC_PUBLIC, "com/pansoft/espdb/bean/Person", null, "java/lang/Object", null);
    
    
    
    /*********************************constructor**********************************************/
    
    MethodVisitor mw = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null,
        null);
    mw.visitVarInsn(ALOAD, 0);
    mw.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V");
    mw.visitInsn(RETURN);
    mw.visitMaxs(1, 1);
    mw.visitEnd();
    
    
    /*************************************field******************************************/
  
    //生成String name字段
    FieldVisitor  fv = cw.visitField(ACC_PUBLIC, "name", "Ljava/lang/String;", null, null);
    AnnotationVisitor  av = fv.visitAnnotation("LNotNull;", true);
    av.visit("value", "abc");
    av.visitEnd();
    fv.visitEnd();

    
    
    /***********************************generate and load********************************************/
    
    byte[] code = cw.toByteArray();
    
    BeanTest loader = new BeanTest();
    Class<?> clazz = loader.defineClass(null, code, 0, code.length);
    
    
    /***********************************test********************************************/
    
    Object beanObj = clazz.getConstructor().newInstance();
    
    clazz.getField("name").set(beanObj, "zhangjg");
    
    String nameString = (String) clazz.getField("name").get(beanObj);
    System.out.println("filed value : " + nameString);
    
    String annoVal = clazz.getField("name").getAnnotation(NotNull.class).value();
    System.out.println("annotation value: " + annoVal);
    
  }
}
上面代碼是完整的代碼, 用於生成一個和以下代碼相對應的class:
public class Person {
    
         @NotNull
         public String name;
    
    }
生成類和構造方法的部分就略過了, 和上面的示例是一樣的。 下面看看字段和字段的註解是如何生成的。 相關邏輯如下:
FieldVisitor  fv = cw.visitField(ACC_PUBLIC, "name", "Ljava/lang/String;", null, null);
    AnnotationVisitor  av = fv.visitAnnotation("LNotNull;", true);
    av.visit("value", "abc");
    av.visitEnd();
    fv.visitEnd();
ClassWriter的visitField方法, 用於定義一個字段。 對應class文件中的一個filed_info 。  

第一個參數是字段的訪問修飾符, 這裏傳入ACC_PUBLIC表示是一個public的屬性。 這個參數和filed_info 中的access_flags相對應。

第二個參數是字段的字段名。 這個參數和filed_info 中的name_index相對應。

第三個參數是字段的描述符, 這個字段是String類型的,它的字段描述符爲 "Ljava/lang/String;" 。 這個參數和filed_info 中的descriptor_index相對應。

第四個參數和泛型相關的, 這裏傳入null, 表示該字段不是泛型的。 這個參數和filed_info 中的 S ignature屬性相對應。

第五個參數是字段的值, 只適用於靜態字段,當前要生成的字段不是靜態的, 所以傳入null 。 這個參數和filed_info 中的ConstantValue屬性相對應。

使用visitField方法定義完當前字段, 返回一個FieldVisitor對象。 下面調用這個對象的visitAnnotation方法, 爲該字段生成註解信息。  visitAnnotation的兩個參數如下:

第一個參數是要生成的註解的描述符, 傳入"LNotNull;" 。

第二個參數表示該註解是否運行時可見。 如果傳入true, 表示運行時可見, 這個註解信息就會生成 filed_info 中的一個RuntimeVisibleAnnotation屬性。 傳入false, 表示運行時不可見, 個註解信息就會生成 filed_info 中的一個RuntimeInvisibleAnnotation屬性 。 

接下來調用上一步返回的AnnotationVisitor對象的visit方法, 來生成註解的值信息。 

ClassWriter的其他重要方法

ClassWriter中還有其他一些重要方法, 這些方法能夠生成class文件中的所有相關信息。 這些方法, 以及對象生成class文件中的什麼信息, 都列在下面:

//定義一個類
  public void visit(
    int version,
    int access,
    String name,
    String signature,
    String superName,
    String[] interfaces)


  //定義源文件相關的信息,對應class文件中的Source屬性
  public void visitSource(String source, String debug)

  //以下兩個方法定義內部類和外部類相關的信息, 對應class文件中的InnerClasses屬性
  public void visitOuterClass(String owner, String name, String desc) 

  public void visitInnerClass(
    String name,
    String outerName,
    String innerName,
    int access)


  //定義class文件中的註解信息, 對應class文件中的RuntimeVisibleAnnotations屬性或者RuntimeInvisibleAnnotations屬性
  public AnnotationVisitor visitAnnotation(String desc, boolean visible)

  //定義其他非標準屬性
  public void visitAttribute(Attribute attr)



  //定義一個字段, 返回的FieldVisitor用於生成字段相關的信息
  public FieldVisitor visitField(
    int access,
    String name,
    String desc,
    String signature,
    Object value)


  //定義一個方法, 返回的MethodVisitor用於生成方法相關的信息
  public MethodVisitor visitMethod(
    int access,
    String name,
    String desc,
    String signature,
    String[] exceptions)
每個方法都是和class文件中的某部分數據相對應的, 如果對class文件的格式比較熟悉的話, 使用ASM生成一個簡單的類, 還是很容易的。

總結

在本文中, 通過使用開源的ASM庫, 動態生成了兩個類。 通過講解這兩個類的生成過程, 可以加深對class文件格式的理解。 因爲ASM庫中的每個API都是對應class文件中的某部分信息的。 如果對class文件格式不熟悉, 可以參考本專欄之前的講解class文件格式的一系列博客。 

本文使用的兩個示例都放在了一個單獨的, 可直接運行的工程中, 該工程已經上傳到我的百度網盤, 這個工程的lib目錄中, 有ASM 4.0的jar包。 和該工程一起打包的, 還有ASM 4.0的源碼和示例程序。

發佈了21 篇原創文章 · 獲贊 1 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章