ASM2.0字节码框架介绍 - 2 http://tianya23.blog.51cto.com/1081650/565662
ASM字节码处理框架是用Java开发的而且使用基于访问者模式生成字节码及驱动类到字节码的转换。这允许开发人员避免直接处理方法字节码中的类常量池及偏移,因此为开发人员隐藏了字节码的复杂性并且相对于其他类似工具如BCEL, SERP, or Javassist提供了更好的性能。
ASM分为几个包更方便灵活地构建。包结构图如图1。
Figure 1. Arrangement of ASM packages
·Core包提供了读/写/转换字节码的API而且是其他包的基础。这个包已经足够生成Java字节码而且能够实现大部分的字节码转换。
·Tree包提供了Java字节码的内存内表示。
·Analysis包为存储在来自Tree包结构中的Java方法字节码提供了基础的数据流分析和类型检查算法。
·Commons包(ASM2.0增加)提供了几个通用的字节码转换和简化字节码生成的适配器。
·Util包包含几个助手类和简单的字节码较验器来方便开发和测试。
·XML包提供了与XML文件相互转换的字节码结构适配器,及兼容SAX而且允许使用XSLT来定义字节码转换方式的适配器。
后面几节会给出ASM框架中Core包的介绍。为了更好地理解这个包的组织结构,你最好有一些在JVM规范中定义的字节码结构的基础了解。下面是较高级别的类文件格式图([*]标识重复的结构)
[1]-------------------------------------------+
| Header and Constant Stack |
+--------------------------------------------+
| [*] Class Attributes |
[2]------------+------------------------------+
| [*] Fields | Field Name, Descriptor, etc |
| +------------------------------+
| | [*] Field Attributes |
[3]------------+------------------------------+
| [*] Methods | Method Name, Descriptor, etc |
| +------------------------------|
| | Method max stack and locals |
| |------------------------------|
| | [*] Method Code table |
| |------------------------------|
| | [*] Method Exception table |
| |------------------------------|
| | [*] Method Code Attributes |
| +------------------------------|
| | [*] Method Attributes |
+-------------+------------------------------+
需要注意的一些地方:
·所有使用在类结构中的描述符,字符串和其他常量都存储在类文件开始的常量堆栈中,来自其他结构的引用是基于堆栈的序号。
·每一个类必须包含头部(包括类名,父类,接口等)和常量堆栈。而其他元素如字段列表/方法列表/属性列表都是可选的。
·每一个方法段包含相同的头信息和最大最小局部变量数的信息,这些是用来校验字节码的。对非抽象和非原生方法,还包含一个方法指令表,一个异常表及代码属性。此外,还可能有其他的方法属性。
·类的每一个属性,成员/方法/方法代码都有自己的名字,具体细节可参考JVM规范的类文件格式部分。这些属性代表字节码的各种信息,如源文件名/内部类/标识(用来存储泛型)/行号/局部变量表和注解。JVM规范也允许定义自定义的属性来包含更多的信息但标准实现的VM不会识别。注:Java5注解实际上已经废弃了那些自定义属性,因为注解在主义上允许你表达更多的东西。
·方法代码表包含JVM的指令列表。一些指令(就像异常/行号/局部变量表)使用代码表中的偏移值并且所有这些偏移的值可能需要在指令从方法代码表中增删时相应调整。
如你所见,字节码转换并不容易。但是,ASM框架减少了潜在的结构复杂性并且提供简化的API允许所有字节码信息的访问和复杂的转换。
基于事件的字节码处理
Core包使用推方案(类似访问者模式,在SAX API就使用了这种模式处理XML)来遍历复杂的字节码结构。ASM定义了几个接口,如ClassVisitor,FieldVisitor,MethodVisitor和AnnotationVisitor。AnnotationVisitor是一个特殊的接口允许你表达层次的注解结构。下面的几幅图显示这些接口是如何相互交互及配合使用实现字节码转换和从字节码获取信息。
Core包逻辑上可怜分为两大部分:
1、字节码生产者,如ClassReader或者按正确顺序调用了上面的访问者类的方法的自定义类。
2、字节码消费者,如输出器(ClassWriter, FieldWriter, MethodWriter, and AnnotationWriter),适配器(ClassAdapter and MethodAdapter)或者其他实现了访问者接口的类。
图2给出了通用生产者/消费者交互过程的时序图。
Figure 2. Sequence diagram for producer-consumer interaction
在这个交互过程中,客户端应用首先创建了ClassReader并调用accept()方法(以ClassVisitor实例作为参数)。然后ClassReader解析类并对每一个字节码断发送“visit”事务给ClassVisitor。对循环的上下文,如成员/方法/注解,ClassVisitor可以创建继续扑克相应接口(FieldVisitor, MethodVisitor, or AnnotationVisitor)的子访问者并返回给生产者。如果生产者接收到一个空值,他简单地忽略类的那部分(如在由访问者驱动的“延迟加载”特性时就不需要解析相应的字节码部分);否则相应的子上下文事件就传递给子访问者实例。当子上下文结束时,生产者调用visitEnd()方法然后移到下一部分。
字节码消费者可以通过手工传递事件给下一个链中的访问者或者使用来自传递所有访问方法给内部的访问者的ClassAdapter/ MethodAdapter的访问者通过“响应链”模式连接起来。这些代理者一方面字节码的消费者方面另一方面也作为字节码的生产者。他们在实现特定的字节码转换时可以修改原始的代理方式:
1、访问调用代理可以在删除类成员/方法/方法指令时被忽略。
2、访问调用参数可以在重命名类/方法/类型时被修改。
3、新访问调用可以在引入新成员/方法/注入新代码到现存代码时被增加。
ClassWriter访问者可以终结整个处理链,他也是最终字节码的生成者。例如:
ClassWriter cw = new ClassWriter(computeMax);
ClassVisitor cc = new CheckClassAdapter(cw);
ClassVisitor tv =
new TraceClassVisitor(cc, new PrintWriter(System.out));
ClassVisitor cv = new TransformingClassAdapter(tv);
ClassReader cr = new ClassReader(bytecode);
cr.accept(cv, skipDebug);
byte[] newBytecode = cw.toByteArray();
在上面的代码中,实现了自定义的类转换并且将结果人作为参数传给TraceClassVisitor的构造函数。TraceClassVisitor打印转换的类并传递给CheckClassAdapter(这是用来作简单的字节校验后传递给ClassWriter)。
大部分的访问方法接收简单的参数如int,boolean和String。在所有的方法中String参数是字节码中常量的引用,ASM使用与JVM一致的方式。例如,所有类名都应该定义在内部格式中。成员和方法描述符应该跟JVM表示一致。泛型信息的表示也类似。这种方式避免了在没有转换时不必要的计算。为了便于构建和解析这样的描述,系统提供了包含一些静态方法的Type类:
·String getMethodDescriptor(Type returnType, Type[] argumentTypes)
·String getInternalName(Class c)
·String getDescriptor(Class c)
·String getMethodDescriptor(Method m)
·Type getType(String typeDescriptor)
·Type getType(Class c)
·Type getReturnType(String methodDescriptor)
·Type getReturnType(Method m)
·Type[] getArgumentTypes(String methodDescriptor)
·Type[] getArgumentTypes(Method m)
注意这些描述符使用了“简单”表示,这意味着不包含泛型信息。泛型信息实际上作为一个单独的字节属性存储,但ASM专门对待这个属性并且在相应访问方法中传递泛型标识串作为参数。这个标识串的值也是参照JVM规范,与Java代码中的泛型定义唯一映射,并且为工具增加获取额外细节的机会。ASM提供了与其他访问者类似的SignatureVisitor, SignatureReader, and SignatureWriter类,如图3所示。
Figure 3. Sequence diagram for Signature classes
Util包中包含了TraceSignatureVisitor,已经实现了SignatureVisitor而且可以将一个标识值转换成Java的泛型定义。下面的例子将一个方法标识转换为Java方法定义。
TraceSignatureVisitor v =
new TraceSignatureVisitor(access);
SignatureReader r = new SignatureReader(sign);
r.accept(v);
String genericDecl = v.getDeclaration();
String genericReturn = v.getReturnType();
String genericExceptions = v.getExceptions();
String methodDecl = genericReturn + " " +
methodName + genericDecl;
if(genericExceptions!=null) {
methodDecl += " throws " + genericExceptions;
}
到目前为止,我们已经讨论了ASM框架的基本设计方式及类结构处理。但最有趣的部分是ASM如何处理方法代码。
访问方法代码
在ASM中,方法定义是由ClassVisitor.visitMethod()来表示,剩下的方法字节码则由MethodVisitor中的许多访问方法来表示。这些方法按照下面的顺序来调用,“*”表示重复的方法而“?”表示方法只能被调用一次。此外,visit...Insn 和visitLabel方法必须按照访问代码的字节码指令顺序调用,而visitTryCatchBlock, visitLocalVariable和visitLineNumber方法必须在标签作为参数传递被访问后才能调用。
注意visitEnd方法必须在方法处理完成后被调用。虽然ClassReader已经做了这一步,但在使用自定义字节码生产者时要注意一点。还要注意如果一个方法包含字节码(也就是说方法是非抽象或非源生的),那么visitCode必须在第一个visit...Insn调用前被调用,而visitMaxs必须在最后一个visit...Insn调用后被调用。
每一个visitIincInsn, visitLdcInsn, visitMultiANewArrayInsn, visitLookupSwitchInsn, and visitTableSwitchInsn方法唯一对应一个字节码指令。剩下的visit...Insn方法对应多个字节码指令,他们的操作码作为第一个方法参数被传入。所有这些操作码常量被定义在Opcodes接口中。这种方式对字节码的解析和格式化非常有效率。不幸的是,这给开发人员生成非法代码的可能,因为ClassWriter不会校验这些限制。但是,还是有一个CheckClassAdapter可以被用来在开发期间测试生成的代码。
另一个机会是对所有字节码生成或转换可以修改方法代码的偏移并且在方法代码中增删额外的指令时应该自动调整。这对所有的跳转伪指令的参数都兼容的,就如try-catch块,行号和局部变量定义及一些特殊属性一样。但是,ASM为开发人员隐藏了这些复杂性。为了定义方法字节码中的位置且不需要使用绝对偏移值,需要传递一个唯一的标签类的实例给visitLabel方法。其他MethodVisitor方法如visitJumpInsn, visitLookupSwitchInsn, visitTableSwitchInsn, visitTryCatchBlock, visitLocalVariable, and visitLineNumber可以使用这些标签实例在visitLabel调用之前,就像实例后在方法后被调用。
续:http://tianya23.blog.51cto.com/1081650/565662