电脑技术学习

ASM2.0字节码框架介绍

dn001
内容: 摘要:
Java的特性如动态类加载和反射使其成为动态语言。然而在许多时候,反射是不够的,而且开发人员需要从非Java源程序中生成字节码,如脚本语言Groovy和BeanShell,或者源数据如ORM配置。当使用已经存在的类时,特别是当没有源程序时,就需要使用一些工具来做如分析类或方法的依赖性以便生成测试度量,或者来检查是否存在问题或反模式。Java5中增加了一些新特性,如如注解和范型,这会影响字节码结果因而需要字节码处理工具特别注意以便保持良好的性能。本文会通过一个最小且最快的Java字节码处理框架来演示。

版权声明:任何获得Matrix授权的网站,转载时请务必保留以下作者信息和链接
作者:Eugene Kuleshov;xMatrix(作者的blog:http://blog.matrix.org.cn/page/xMatrix)
原文:http://www.onjava.com/pub/a/onjava/2005/08/17/asm3.html
译文:http://www.matrix.org.cn/resource/article/44/44220_ASM+Bytecode+Framework.html
关键字:ASM;Bytecode;Framework

框架结构
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调用之前,就像实例后在方法后被调用。

上面的内容看起来很复杂,好像需要很深奥的字节码指令知识。但是在编译的类上使用ASMifierClassVisitor就可以让你知道如何用ASM生成给定的字节码。此外,在两个编译的类上(一个原始的和另一个应用特定的转换)使用然后进行比较就可以给出什么样的ASM调用应该被使用在转换器上。这个过程在几篇文章中已经详细解释了(可以参看最后的资源部分)。目前已经有了Eclipse使用的插件了,如图4,提供了从Java源生成ASM代码及比较ASMifier输出的良好支持,还包含了上下文字节码的参考。


Figure 4. Eclipse ASM plugin (Click on the picture to see a full-size image)

用ASM的访问者来跟踪类的依赖

已经有一些文章介绍了如何用ASM生成字节码。现在,我们来看一下如何用ASM分析已有的类。我们来做一个有趣的应用来获取给定的.jar文件中使用的外部类和包。简单起见,这个例子仅获取外部的依赖而不会取依赖的类型(如父类/方法参数/局部变量类型等)。
仅为分析,我们不会创建那些注解/成员/方法的子访问者实例。所有使用的访问者(包括类和标识访问者)都在一个类中实现:
public class DependencyVisitor implements 
AnnotationVisitor, SignatureVisitor,
ClassVisitor, FieldVisitor, MethodVisitor {
...


在这个例子中,我们会跟踪包之间的依赖,因此私有类必须包含包名:
 private String getGroupKey(String name) {
int n = name.lastIndexOf('/');
if(n>-1) name = name.substring(0, n);
packages.add(name);
return name;
}


为了获取依赖关系,访问者接口如ClassVisitor, AnnotationVisitor, FieldVisitor, and MethodVisitor应该选择性地集成方法的参数。几个常见的样例如下:
 private void addName(String name) {
if(name==null) return;
String p = getGroupKey(name);
if(current.containsKey(p)) {
current.put(p, current.get(p)+1);
} else {
current.put(p, 1);
}
}


在这个例子中,current是依赖的当前包。
另一个例子是类型描述符(注解/枚举/成员类型/newarray指令的参数等);如Ljava/lang/String;, J, and [[[I。这些可以用Type.getType( desc)来获取内部格式的类名:
 private void addDesc(String desc) {
addType(Type.getType(desc));
}

private void addType(Type t) {
switch(t.getSort()) {
case Type.ARRAY:
addType(t.getElementType());
break;
case Type.OBJECT:
addName(t.getClassName().replace('.','/'));
break;
}
}


在方法定义中的方法描述法及激活指令中的描述参数类型及返回类型。可以通过Type.getReturnType(methodDescriptor) 和Type.getArgumentTypes(methodDescriptor)来解析并取得参数和返回类型。
 private void addMethodDesc(String desc) {
addType(Type.getReturnType(desc));
Type[] types = Type.getArgumentTypes(desc);
for(int i = 0; i < types.length; i++) {
addType(types[ i]);
}
}


而使用在许多“访问方法中的用来定义Java5泛型信息的标识参数是个特例。如果存在,这个参数重写描述符参数并包含编码后的泛型信息。可以被用SignatureReader来解析这个值。所以我们可以实现SignatureVisitor来被每一个标识工件来调用。
 private void addSignature(String sign) {
if(sign!=null) {
new SignatureReader(sign).accept(this);
}
}

private void addTypeSignature(String sign) {
if(sign!=null) {
new SignatureReader(sign).acceptType(this);
}
}


实现ClassVisitor接口的方法,如such as visit(), visitField(), visitMethod(), and visitAnnotation()就可以获取在父类/接口/成员类型/方法参数/返回值/异常上的依赖信息,就如注解一样。例如:
 public void visit(int version, int access, 
String name, String signature,
String superName, String[] interfaces) {
String p = getGroupKey(name);
current = groups.get(p);
if(current==null) {
current = new HashMap();
groups.put(p, current);
}

if(signature==null) {
addName(superName);
addNames(interfaces);
} else {
addSignature(signature);
}
}

public FieldVisitor visitField(int access,
String name, String desc,
String signature, Object value) {
if(signature==null) {
addDesc(desc);
} else {
addTypeSignature(signature);
}
if(value instanceof Type) {
addType((Type) value);
}
return this;
}

public MethodVisitor visitMethod(int access,
String name, String desc,
String signature, String[] exceptions) {
if(signature==null) {
addMethodDesc(desc);
} else {
addSignature(signature);
}
addNames(exceptions);
return this;
}

public AnnotationVisitor visitAnnotation(
String desc, boolean visible) {
addDesc(desc);
return this;
}


实现MethodVisitor接口的方法就可以获取关于参数注解类型和使用在可以使用对象引用的字节码指令上的依赖:
 public AnnotationVisitor 
visitParameterAnnotation(int parameter,
String desc, boolean visible) {
addDesc(desc);
return this;
}

/**
* Visits a type instruction
* NEW, ANEWARRAY, CHECKCAST or INSTANCEOF.
*/
public void visitTypeInsn(int opcode,
String desc) {
if(desc.charAt(0)=='[') {
addDesc(desc);
} else {
addName(desc);
}
}

/**
* Visits a field instruction
* GETSTATIC, PUTSTATIC, GETFIELD or PUTFIELD.
*/
public void visitFieldInsn(int opcode,
String owner, String name, String desc) {
addName(owner);
addDesc(desc);
}

/**
* Visits a method instruction INVOKEVIRTUAL,
* INVOKESPECIAL, INVOKESTATIC or
* INVOKEINTERFACE.
*/
public void visitMethodInsn(int opcode,
String owner, String name, String desc) {
addName(owner);
addMethodDesc(desc);
}

/**
* Visits a LDC instruction.
*/
public void visitLdcInsn(Object cst) {
if(cst instanceof Type) {
addType((Type) cst);
}
}

/**
* Visits a MULTIANEWARRAY instruction.
*/
public void visitMultiANewArrayInsn(
String desc, int dims) {
addDesc(desc);
}

/**
* Visits a try catch block.
*/
public void visitTryCatchBlock(Label start,
Label end, Label handler, String type) {
addName(type);
}


现在我们可以用DependencyVisitor来获取整个.jar文件的依赖关系了。例如:
 DependencyVisitor v = new DependencyVisitor();
ZipFile f = new ZipFile(jarName);
Enumeration<? extends ZipEntry> en = f.entries();
while(en.hasMoreElements()) {
ZipEntry e = en.nextElement();
String name = e.getName();
if(name.endsWith(".class")) {
ClassReader cr =
new ClassReader(f.getInputStream(e));
cr.accept(v, false);
}
}


可以用很多不同的方式来表示得到的信息。一种方式是构建依赖树并计算相关数据或者创建可视化的东西。例如,图5显示了ant1.6.5 jar包中的依赖关系的可视化表现,这是我使用一些简单的Java2D代码写的。下面的图在水平轴上显示包,在垂直轴上显示依赖。阴影部分表示包被多次引用。


Figure 5. Dependencies in ant.1.6.5.jar, as discovered with ASM

这个工具的全部代码会被包含在下一个ASM发布中。你可以从ASM CVS获取。


ASM1.x后的改变

如果你没有使用ASM1.x可以略过这个段。2.0中主要的结构变化是所有J2SE5.0的特性都被内建到访问者/过滤器的事件流中。因此新的API允许你用更轻便和自然的方式来处理泛型和注解。不需要显式创建注解属性实例,因为在事件流中已经包含了泛型和注解数据。例如,在1.x,ClassVisitor接口如下使用:
 CodeVisitor visitMethod(int access, String name, 
String desc, String[] exceptions,
Attribute attrs);
This has been split into several methods in ASM 2.0:
在2.0中已经分为多个方法:
MethodVisitor visitMethod(int access,
String name, String desc, String signature,
String[] exceptions)

AnnotationVisitor visitAnnotation(String desc,
boolean visible)

void visitAttribute(Attribute attr)


在1.x API中,为了定义泛型信息,你必须创建SignatureAttribute的实例,而定义注解你需要RuntimeInvisibleAnnotations, RuntimeInvisibleParameterAnnotations, RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations, and AnnotationDefault的实例。然后你可以将这些实例放在相应的访问方法的attrs参数中。
在2.0,增加了新的标识参数来表示泛型信息。新的AnnotationVisitor接口被用来处理所有的注解。不再需要创建attrs集合了,而且注解数据是强类型的。然而在移植现有代码时,特别是在“适配器类被使用时,必须注意确保所有来自适配器的方法需要重写来适应新的标识,因为编译器不用对这种情况给出警告。


ASM2.0还有些其他的改变。
1、增加了新的接口FieldVisitor 和AnnotationVisitor
2、CodeVisitor合并到MethodVisitor中了。
3、在MethodVisitor中增加了visitCode()方法简化检测首个指令。
4、Constants接口重构为Opcodes。
5、所有来自attrs包的属性被包含到ASM的事件模型中。
6、TreeClassAdapter and TreeCodeAdapter被包含到ClassNode and MethodNode中。
7、增加LabelNode类使指令集合的元素成为AbstractInsnNode的通用类型。
通常,建议使用如JDiff这样的工具来比较两个版本之间的区别。

小结
ASM2.0为开发人员屏蔽了字节码的复杂性,因而使开发人员更有效在字节码级别上使用Java的特性。这个框架不仅允许你转换和生成字节码,而且可以从现有的类中取得具体的信息。他的API继续改善,现在已经包含了J2SE5.0中的泛型和注解。接下来,还会增加Mustang(J2SE6)中的新特性。

资源
·Java Virtual Machine Specification Java虚拟机规范
&#8226;·"“修订的类文件格式(JVM规范的第4章)。包含J2SE5.0中支持的JSR-14/JSR-175/JSR-201中要求的修改及其他小的更正和调整。
·“使用ASM工具集来处理字节码
·“使用ASM工具集来创建和读写J2SE5.0注解
·字节码指令(BCI)。

Eugene Kuleshov是一个独立咨询师,拥有超过15年的软件设计开发经验。

Java, java, J2SE, j2se, J2EE, j2ee, J2ME, j2me, ejb, ejb3, JBOSS, jboss, spring, hibernate, jdo, struts, webwork, ajax, AJAX, mysql, MySQL, Oracle, Weblogic, Websphere, scjp, scjd 摘要:
Java的特性如动态类加载和反射使其成为动态语言。然而在许多时候,反射是不够的,而且开发人员需要从非Java源程序中生成字节码,如脚本语言Groovy和BeanShell,或者源数据如ORM配置。当使用已经存在的类时,特别是当没有源程序时,就需要使用一些工具来做如分析类或方法的依赖性以便生成测试度量,?

标签: