框架结构
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();
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;
}
public class DependencyVisitor implements
AnnotationVisitor, SignatureVisitor,
ClassVisitor, FieldVisitor, MethodVisitor {
...
private String getGroupKey(String name) {为了获取依赖关系,访问者接口如ClassVisitor, AnnotationVisitor, FieldVisitor, and MethodVisitor应该选择性地集成方法的参数。几个常见的样例如下:
int n = name.lastIndexOf('/');
if(n>-1) name = name.substring(0, n);
packages.add(name);
return name;
}
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);
}
}
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;
}
}
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]);
}
}
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);
}
}
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;
}
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 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);
}
}
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虚拟机规范•·"“修订的类文件格式”(JVM规范的第4章)。包含J2SE5.0中支持的JSR-14/JSR-175/JSR-201中要求的修改及其他小的更正和调整。
·“使用ASM工具集来处理字节码”
·“使用ASM工具集来创建和读写J2SE5.0注解”
·字节码指令(BCI)。
Eugene Kuleshov是一个独立咨询师,拥有超过15年的软件设计开发经验。
版权声明:任何获得Matrix授权的网站,转载时请务必保留以下作者信息和链接
原文:http://www.javaworld.com/
译文:http://www.matrix.org.cn/