概述
本專欄前面的文章,主要詳細講解了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的源碼和示例程序。