ASM技术研究 ASM是什么
ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件 也可以在类被加载入 Java 虚拟机之前动态改变类行为。ASM 提供类似于 BCEL 和 SERP 之类的工具包的功能 但是被设计得更小巧、更快速 这使它适用于实时代码插装。
ASM原理Java class 被存储在严格格式定义的 .class 文件里 这些类文件拥有足够的元数据来解析类中的所有元素 类名称、方法、属性以及 Java 字节码 指令 。ASM 就是从类文件中读入这些信息 并且提供了访问和修改这些信息的接口 进而改变类的行为。对于 ASM 来说 Java class 被描述为一棵树 ASM使用 “Visitor” 访问者 模式遍历整个二进制结构 以事件驱动的处理方式将对类信息的操作接口传递给用户 访问者 用户只需要关注于对其业务有意义的部分 而不必了解 Java 类文件格式的所有细节。
ASM使用方式如果对类的修改是一次性的且原始类信息是可知的 那么可以使用ASM直接编译出修改过的class并将其保存到硬盘 之后运行期不再依赖ASM 和普通的类没有区别 这种方式通常需要自定义ClassLoader 如ConverClassLoader等。
如果不希望改变原有的类文件 只是在运行期添加或修改一些类信息的话 比如动态代理、AOP等 可以使用java的Instrument技术 启动时往 Java 虚拟机中挂上一个用户定义的 hook 程序 在装入特定类的时候使用ASM改变特定类的字节码 从而改变该类的行为 举例如下
public static void premain(String agentArgs, Instrumentation inst) { inst.addTransformer(new ClassFileTransformer() { public byte[] transform(ClassLoader l, String name, Class c, ProtectionDomain d, byte[] b) throws IllegalClassFormatException { ClassReader cr new ClassReader(b); ClassWriter cw new ClassWriter(cr, 0); ClassVisitor cv new ChangeVersionAdapter(cw); cr.accept(cv, 0); return cw.toByteArray(); } });}
另外 当然也可以在程序的任何位置使用ASM 这些都是没有限制的 ASM从本质上说也是一个java类库 当然可以随时使用其功能。
类信息访问次序ASM是以事件驱动的处理方式将类信息传递给用户 用户要接收这些信息需要实现特定的Visitor 访问者 Visitor中的方法是由ASM依据对类信息的遍历顺序进而调用的 ASM中定义了多种Visitor 以ClassVisitor为例 方法调用的顺序
visit
visitSource?
visitOuterClass?
( visitAnnotation | visitAttribute)*
( visitInnerClass | visitField | visitMethod)*
visitEnd
这就是说 visit 方法必须最先被调用 然后是最多调用一次 visitSource 方法 然后是最多调用一次 visitOuterClass 方法。然后是 visitAnnotation 和 visitAttribute 方法以任意顺序被调用任意多次。再然后是以任任意顺序调用 visitInnerClass ,visitField 或 visitMethod 方法任意多次。最终 调用一次 visitEnd 方法结束对类的访问 当然我们通过打印的方式或通过官方提供的TraceClassVisitor工具也很容易了解到类信息的访问顺序。
编写自己的类访问者 需要继承自ClassVisitor 定义基类
import org.objectweb.asm.ClassVisitor;import org.objectweb.asm.Opcodes;public abstract class AbstractClassVisitor extends ClassVisitor { public static final int ASM_API_VERSION Opcodes.ASM5; public AbstractClassVisitor(ClassVisitor delegate) { super(ASM_API_VERSION, delegate); }}添加一个属性
import org.objectweb.asm.*;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;public class AddField extends AbstractClassVisitor { private boolean isPresent; private int access; private String name; private String desc; private Object value; public AddField(int access, String name, String desc, Object value, ClassVisitor delegate) { super(delegate); this.access access; this.name name; this.desc desc; this.value value; } Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { if (name.equals(this.name)) { isPresent true; } return super.visitField(access, name, desc, signature, value); } Override public void visitEnd() { if (!isPresent) { FieldVisitor fv super.visitField(access, name, desc, null, value); if (fv ! null) { fv.visitEnd();//不是原有的属性 故不会有事件发出的 自己 end 掉。 } } super.visitEnd(); } public static void main(String[] args) throws Exception { ClassReader classReader new ClassReader(new FileInputStream( TestBean.class )); ClassWriter classWriter new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS); ClassVisitor addField new AddField(Opcodes.ACC_PUBLIC Opcodes.ACC_STATIC Opcodes.ACC_FINAL, newField2 , Type.getDescriptor(String.class), newValue2 , classWriter); classReader.accept(addField, ClassReader.SKIP_DEBUG); byte[] newClass classWriter.toByteArray(); File newFile new File( TestBean.class ); new FileOutputStream(newFile).write(newClass); }}
上述代码中 使用ClassReader从一个类文件中读取到类的二进制形式 ClassWriter用于完成二进制文件的写操作 AddField是业务类 其visitEnd()方法被最后触发 在这里加入下面的代码 完成了属性的添加
FieldVisitor fv super.visitField(access, name, desc, null, value);if (fv ! null) { fv.visitEnd();//不是原有的属性 故不会有事件发出的 自己 end 掉。}
如果要删除一个属性 则在visitField方法中操作 直接返回null即可 如
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { if (name.equals(this.name)) { isPresent true; return null;// 返回 null 即可删除此属性 } return super.visitField(access, name, desc, signature, value);}添加接口
public class AddInterfaces extends AbstractClassVisitor { private Set newInterfaces; public AddInterfaces(ClassVisitor cv, Set newInterfaces) { super(cv); this.newInterfaces newInterfaces; } public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { Set String ints new HashSet(newInterfaces); ints.addAll(Arrays.asList(interfaces)); super.visit(version, access, name, signature, superName, ints.toArray(new String[ints.size()])); } public static void main(String[] args) throws Exception { ClassReader cr new ClassReader(new FileInputStream( TestBean.class )); ClassWriter cw new ClassWriter(cr, ClassWriter.COMPUTE_MAXS); Set set new HashSet(); set.add(Type.getInternalName(Runnable.class)); set.add(Type.getInternalName(Comparable.class)); AddInterfaces cv new AddInterfaces(cw, set); cr.accept(cv, ClassReader.SKIP_DEBUG); byte[] bytes cw.toByteArray(); // save to disk new FileOutputStream(new File( TestBean.class )).write(bytes); }}
使用ASM我们几乎可以操作class的所有信息 只要我们知道这些信息在哪些方法中被触发 进而我们在合适的时机和地方对其进行相关的操作即可 比如类的版本信息会在visit方法中被触发访问 我们就可以在此方法里对其进行操作 上面的代码中对TestBean类添加了两个接口Runnable和Comparable 因为对接口的也访问在visit中被触发 所以我们在这里 对其进行了接口的添加
Set String ints new HashSet(newInterfaces); ints.addAll(Arrays.asList(interfaces)); super.visit(version, access, name, signature, superName, ints.toArray(new String[ints.size()]));
当然接口的实现也是需要添加的 这里省略。另外如果要删除一个类的接口 可以如此
super.visit(version, access, name, signature, superName, null);//删除接口添加一个方法
public class AddMethod extends AbstractClassVisitor { private boolean isInterface; private boolean isPresent; private String name; private String desc; public AddMethod(String name, String desc, ClassVisitor delegate) { super(delegate); this.name name; this.desc desc; } Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { isInterface (access ACC_INTERFACE) ! 0; super.visit(version, access, name, signature, superName, interfaces); } public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if (name.equals(this.name) desc.equals(this.desc)) { isPresent true; } return super.visitMethod(access, name, desc, signature, exceptions); } public void visitEnd() { if (!isInterface !isPresent) { MethodVisitor mv super.visitMethod(ACC_PUBLIC ACC_STATIC, name, desc, null, null); mv.visitCode(); Label l0 new Label(); mv.visitLabel(l0); mv.visitLineNumber(17, l0); mv.visitFieldInsn(GETSTATIC, java/lang/System , out , Ljava/io/PrintStream; ); mv.visitLdcInsn( liming ); mv.visitMethodInsn(INVOKESTATIC, asm/TestBean , sayHello , (Ljava/lang/String;)Ljava/lang/String; , false); mv.visitMethodInsn(INVOKEVIRTUAL, java/io/PrintStream , println , (Ljava/lang/String;)V , false); Label l1 new Label(); mv.visitLabel(l1); mv.visitLineNumber(18, l1); mv.visitInsn(RETURN); Label l2 new Label(); mv.visitLabel(l2); mv.visitLocalVariable( args , [Ljava/lang/String; , null, l0, l2, 0); mv.visitMaxs(1, 1);//设置ClassWriter.COMPUTE_MAXS 或 ClassWriter.COMPUTE_FRAMES 此处的值会被忽略 但此方法必需显示调用一下 mv.visitEnd(); } super.visitEnd(); } public static void main(String[] args) throws Exception { ClassReader classReader new ClassReader(new FileInputStream( TestBean.class )); ClassWriter classWriter new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS); String methodDescriptor Type.getMethodDescriptor(Type.getType(Void.class), Type.getType(Integer.class), Type.getType(String.class)); System.out.println( 方法描述符 methodDescriptor); AddMethod addMethod new AddMethod( newMethod , methodDescriptor, classWriter); classReader.accept(addMethod, ClassReader.SKIP_DEBUG); byte[] newClass classWriter.toByteArray(); File newFile new File( TestBean.class ); new FileOutputStream(newFile).write(newClass); }}
要添加一个方法需要对类的字节码有较为深入的了解 需要了解其压栈和出栈的知识以及变量使用等技术 幸运的是ASM提供了TraceClassVisitor工具可以帮助我们轻松获取这部分的样例代码 我们只需要在上面稍加修改就可以。
删除一个方法同删除属性的操作类似 在visitMethod处直接返回null即可
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if (name.equals(mName)) { return null;// do not delegate to next visitor - this removes the method } return super.visitMethod(access, name, desc, signature, exceptions);}生成一个完整类并加载
static void generateClass() throws Exception { ClassWriter cw new ClassWriter(ClassWriter.COMPUTE_FRAMES); cw.visit(V1_5, ACC_PUBLIC, pkg/MyObject , null, java/lang/Object , null); cw.visitField(ACC_PUBLIC ACC_FINAL, a , I , null, new Integer(-1)).visitEnd(); cw.visitField(ACC_PRIVATE ACC_FINAL ACC_STATIC, b , I , null, new Integer(0)).visitEnd(); cw.visitField(ACC_PUBLIC ACC_STATIC, c , I , null, new Integer(100)).visitEnd(); cw.visitField(ACC_PUBLIC ACC_FINAL ACC_STATIC, other , I , null, null).visitEnd(); cw.visitMethod(ACC_PUBLIC ACC_ABSTRACT, compareTo , (Ljava/lang/Object;)I , null, null).visitEnd(); cw.visitEnd(); final byte[] bytes cw.toByteArray(); // save to disk new FileOutputStream(new File( /home/wangpl/Desktop/asm-test/new/MyObject.class )).write(bytes); // runtime proxy loader class MyClassLoader extends ClassLoader { Override protected Class findClass(String name) throws ClassNotFoundException { if (name.equals( pkg.MyObject )) { byte[] b bytes; return defineClass(name, b, 0, b.length); } return super.findClass(name); } } Class cls new MyClassLoader().loadClass( pkg.MyObject ); // 反射 其属性 c 的值 上面设置为 100 System.out.println(cls.getField( c ).get(cls)); }
上面的代码 我们使用ClassWriter 手动触发需要的方法以驱动其动态生成一个class 触发的动作原本是ClassReader做的 由于是新生成类 只能手动调用各方法。
官方代码生成类// creates a ClassWriter for the Example public class which inherits from Object ClassWriter cw new ClassWriter(ClassWriter.COMPUTE_MAXS); cw.visit(V1_1, ACC_PUBLIC, Example , null, java/lang/Object , null); // creates a MethodWriter for the (implicit) constructor MethodVisitor mw cw.visitMethod(ACC_PUBLIC, init , ()V , null, null); // pushes the this variable mw.visitVarInsn(ALOAD, 0); // invokes the super class constructor mw.visitMethodInsn(INVOKESPECIAL, java/lang/Object , init , ()V , false); mw.visitInsn(RETURN); // this code uses a maximum of one stack element and one local variable mw.visitMaxs(1, 1); mw.visitEnd(); // creates a MethodWriter for the main method mw cw.visitMethod(ACC_PUBLIC ACC_STATIC, main , ([Ljava/lang/String;)V , null, null); // pushes the out field (of type PrintStream) of the System class mw.visitFieldInsn(GETSTATIC, java/lang/System , out , Ljava/io/PrintStream; ); // pushes the Hello World! String constant mw.visitLdcInsn( Hello world! ); // invokes the println method (defined in the PrintStream class) mw.visitMethodInsn(INVOKEVIRTUAL, java/io/PrintStream , println , (Ljava/lang/String;)V , false); mw.visitInsn(RETURN); // this code uses a maximum of two stack elements and two local variables mw.visitMaxs(2, 2); mw.visitEnd();打印类的源码信息
public class ClassInfoPrinter extends AbstractClassVisitor { public ClassInfoPrinter(ClassVisitor delegate) { super(delegate); } public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { System.out.println(name extends superName { ); } public AnnotationVisitor visitAnnotation(String desc, boolean visible) { return null; } public void visitInnerClass(String name, String outerName, String innerName, int access) { } public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { System.out.println( desc name); return null; } public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { System.out.println( name desc); return null; } public void visitEnd() { System.out.println( } ); } public static void main(String[] args) throws Exception { ClassReader classReader new ClassReader(new FileInputStream( /home/wangpl/Desktop/asm-test/TestBean.class )); ClassVisitor classVisitor new ClassInfoPrinter(null); classReader.accept(classVisitor, ClassReader.SKIP_DEBUG); }}
上面的代码可以打印出TestBean.class的基础信息 如类版本、属性、方法及其描述等等 由于不需要写操作 故不要代理给ClassWriter。
ASM进行AOPAop 无论概念有多么深奥。它无非就是一个“Propxy模式”。被代理的方法在调用前后作为代理程序可以做一些预先和后续的操作。这一点想必读者都能达到一个共识。因此要想实现 Aop 的关键是 如何将我们的代码安插到被调用方法的相应位置。
而Aop 最快最直接的方法即是将代码直接安插到类的相应位置。先看一段最简单的代码。
在介绍指令之前我先简单说明一下 JVM 的运行机制。首先可以简单的将 JVM 虚拟机看作是一个 CPU。作为 CPU 都要有一个入口程序。在我们的电脑中主板的 Bioss 程序就是充当这个角色 而在JVM 中 Main方法来充当这一角色。CPU 在运行程序的时会将程序数据放入它的几个固定存储器 我们称它们为寄存器。CPU 对数据的所有计算都针对寄存器。而 JVM 并不具备这一特征 它采用的是堆结构。
比方说计算 “a b” 在 CPU 中需要两个寄存器。首先将“1”载入第一个寄存器 其次将另外一个“1”载入第二个寄存器 然后调用相应的加法指令将两个寄存器中的数据相加。相加的结果会保存在另外一个寄存器上。而在 JVM 中首先将第一个“1”push到堆栈中 其次在将另外一个“1”push到堆栈中 紧接着调用ADD指令。这个指令会取出这两个数字相加然后将结果再次放入堆栈中。经过运算之后堆栈中的两个“1”已经不存在了 在堆栈顶端有一个新的值“2”。JVM 所有计算都是在此基础之上完成的。
在 Java 中每一个方法在执行的时候 JVM 都会为其分配一个“帧” 帧是用来存储方法中计算所需要的所有数据。其中第 0 个元素就是 “this” 如果方法有参数传入会排在它的后面。
ALOAD_0
这个指令是LOAD系列指令中的一个 它的意思表示装载当前第 0 个元素到堆栈中。代码上相当于“this”。而这个数据元素的类型是一个引用类型。这些指令包含了 ALOAD ILOAD LLOAD FLOAD DLOAD。区分它们的作用就是针对不用数据类型而准备的LOAD指令 此外还有专门负责处理数组的指令 SALOAD。
invokespecial
这个指令是调用系列指令中的一个。其目的是调用对象类的方法。后面需要给上父类的方法完整签名。“#8”的意思是 .class 文件常量表中第8个元素。值为 “java/lang/Object.””:()V”。结合ALOAD_0。这两个指令可以翻译为 “super()”。其含义是调用自己的父类构造方法。
GETSTATIC
这个指令是GET系列指令中的一个其作用是获取静态字段内容到堆栈中。这一系列指令包括了 GETFIELD、GETSTATIC。它们分别用于获取动态字段和静态字段。
IDC
这个指令的功能是从常量表中装载一个数据到堆栈中。
invokevirtual
也是一种调用指令 这个指令区别与 invokespecial 的是它是根据引用调用对象类的方法。这里有一篇文章专门讲解这两个指令 “http://wensiqun.iteye.com/blog/1125503”。
RETURN
这也是一系列指令中的一个 其目的是方法调用完毕返回 可用的其他指令有 IRETURN DRETURN ARETURN等 用于表示不同类型参数的返回。
详细参考 http://my.oschina.net/u/1166271/blog/162796
原先的业务代码示例
public class TestBean { public void halloAop() { System.out.println( Hello Aop );}
定义AOP拦截器
public class AopInterceptor { public static void beforeInvoke() { System.out.println( before ); public static void afterInvoke() { System.out.println( after ); public static void main(String[] args) throws Exception { ClassReader classReader new ClassReader(new FileInputStream( /home/wangpl/mine/WORK_SPACE/idea_work/Eden/test/target/test-classes/asm/TestBean.class )); ClassWriter classWriter new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS); ClassVisitor visitor new AopClassAdapter(Opcodes.ASM5, classWriter); classReader.accept(visitor, ClassReader.SKIP_DEBUG); new FileOutputStream( /home/wangpl/Desktop/a/TestBean.class ).write(classWriter.toByteArray());}
定义AOP的访问者AopClassAdapter 对原类进行泛化
class AopClassAdapter extends ClassVisitor implements Opcodes { public AopClassAdapter(int api, ClassVisitor cv) { super(api, cv); public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { //更改类名 并使新类继承原有的类。 super.visit(version, access, name _Tmp , signature, name, interfaces); MethodVisitor mv super.visitMethod(ACC_PUBLIC, init , ()V , null, null); mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKESPECIAL, name, init , ()V ); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if ( init .equals(name)) return null; if (!name.equals( halloAop )) return null; MethodVisitor mv super.visitMethod(access, name, desc, signature, exceptions); return new AopMethod(this.api, mv);class AopMethod extends MethodVisitor implements Opcodes { public AopMethod(int api, MethodVisitor mv) { super(api, mv); public void visitCode() { super.visitCode(); this.visitMethodInsn(INVOKESTATIC, asm/AopInterceptor , beforeInvoke , ()V ); public void visitInsn(int opcode) { if (opcode RETURN) { mv.visitMethodInsn(INVOKESTATIC, asm/AopInterceptor , afterInvoke , ()V ); super.visitInsn(opcode);}ASM的实用工具
除ClassVisitor、ClassReader、ClassWriter等一些核心接口之外 ASM还提供了一些实用的工具 这些工具在开发class生成器和适配器中非常有用。
TypeType对象代表了一个Java类型 同时也包含了代表了基本类型的静态变量 如Type.INT_TYPE对应Int 它提供了方便方法可以从在java类型和ASM类型之间的转换 如Type.getType(String.class).getInternalName()返回String类的internal name ”java/lang/String” Type.getType(String.class).getDescriptor()返回String类的descriptor ”Ljava/lang/String” Type.getArgumentTypes(“(I)V”)返回一个包含单个元素类型TYPE.INT_TYPE Type.getReturnType(“(I)V”)返回Type.VOID_TYPE对象。
TraceClassVisitor通过ClassWriter我们无法确定ASM生成的或转换的class是否符合我们预期 因为ClassWriter返回的只是字节数组不做任何检查 而TraceClassVisitor提供了文本的展现形式 该类也继承自ClassVisitor 并代理了所有调用到其他ClassVisitor 在委托给其下一个visitor之前 打印出每次访问操作的全过程 用法如下
TraceClassVisitor tcv new TraceClassVisitor(cv, new ASMifier(), new PrintWriter(System.out));//TraceClassVisitor tcv new TraceClassVisitor(cv, new Textifier(), new PrintWriter(System.out));classReader.accept(tcv, ClassReader.SKIP_DEBUG);
Tip:
其中使用ASMifier可以展现一个访问者访问具体类信息的过程 另外一种方式是Textifier ASMifier在我们自己定制访问者时是十分有用的 比如 我们要生成一个方法就可以先写一个包行此方法的java类编译成class 然后使用TraceClassVisitor去打印出这个方法的访问过程 我们就可以清晰地看到生成此方法的访问者逻辑了。
ASMifier也可以通过命令行直接使用 这是因为ASMifier本身包含了main方法入口 内部逻辑和上面的也是一致的 通过命令行可以不要编写代码就查看类信息 用法如下
java -classpath asm.jar:asm-util.jar org.objectweb.asm.util.ASMifier java.lang.Runnable
其中java.lang.Runnable是要查看的类全名或类文件的相对路径。
同样 Textifier也有类似的用法。
前面我们已经知道访问者的方法是被ASM按顺序触发执行 尤其是要传递到ClassWriter中进行类的组织重写的时候 顺序是很重要的 错误的顺序可能是ClassWriter写出类无法使用 然而ClassWriter本身不会检查调用是否以合适的顺序和有效的参数在进行 为了尽可能探测到一些错误 可以使用CheckClassAdapter。像TraceClassVisitor一样 该类也继承自ClassVisitor 并代理了所有调用到其他ClassVisitor 在委托给其下一个visitor之前 检查他的方法以合适的顺序和有效的参数被调用 用法举例
InputStream is ...; // get bytes for the source classClassReader cr new ClassReader(is);ClassWriter cw new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);ClassVisitor cv new MyClassAdapter(new CheckClassAdapter(cw));cr.accept(cv, ClassReader.SKIP_DEBUG);StringWriter sw new StringWriter();PrintWriter pw new PrintWriter(sw);CheckClassAdapter.verify(new ClassReader(cw.toByteArray()), false, pw);assertTrue(sw.toString(), sw.toString().length() 0);
NOTE:
TraceClassVisitor 多用在自定义ClassVisitor之前以打印正在访问的信息 而CheckClassAdapter则多用于其后以检查访问的顺序是否正确。
Tree API就是针对Class,Method,Field等属性进行了一个包装 可以让我们用面向对像的形式来操作字节码 但也不要负于太高的期望 Tree API官方称会有30%的性能损失 并且需要更多的内存开销 对于一些运行期实时性的字节码修改 实时动态代理等 对性能的要求往往会比较高 使用Tree API可能会达不到要求 况且个人实践发现其宣称的便利性效果也不是很理想 关于这方面不作深入讨论。
当使用ASM修改字节码后有时候不能debug调试 这是因为ASM修改字节码的时候丢掉了行号line number信息 这个设置在 cr.accept(cv, ClassReader.SKIP_DEBUG); 就是这个ClassReader.SKIP_DEBUG参数 当使用 ClassReader.SKIP_DEBUG和ClassReader.SKIP_CODE时都会丢失行号信息 使用其它的如 SKIP_FRAMES 和 EXPAND_FRAMES 则不会 更保险的 直接 设置为 0 则所有的都不会禁用 即开启所有class访问。
这里有一篇IBM的技术文章 比较详细的介绍了Class规范和ASM的相关技术
http://www.ibm.com/developerworks/cn/java/j-lo-asm30/
想了解更多
http://asm.ow2.org/
本文链接: http://asmresearch.immuno-online.com/view-683705.html