概述
本专栏前面的文章,主要详细讲解了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方法定义一个类。
第六个参数是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();
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 -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示例二: 生成字段, 并给字段加注解
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 。
ClassWriter的其他重要方法
//定义一个类
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生成一个简单的类, 还是很容易的。
总结
更多关于深入理解Java的文章, 请关注我的专栏 : http://blog.youkuaiyun.com/column/details/zhangjg-java-blog.html
更多关于Java和Android等其他技术的文章, 请关注我的博客: http://blog.youkuaiyun.com/zhangjg_blog