2.2.2解析类
解析一个已存在的类仅需要ClassReader这个组件。下面让我们以一个实例来展示如何解析类。假设,我们想要打印一个类的内容,我们可以使用javap这个工具。第一步,实现ClassVisitor这个接口,用来打印类的信息。下面是一个简单的实现:
public class ClassPrinter implements ClassVisitor {
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
System.out.println(name + " extends " + superName + " {");
}
public void visitSource(String source, String debug) {
}
public void visitOuterClass(String owner, String name, String desc) {
}
public AnnotationVisitor visitAnnotation(String desc,
boolean visible) {
return null;
}
public void visitAttribute(Attribute attr) {
}
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("}");
}
}
第二步,将ClassPrinter和ClassReader结合起来,这样,ClassReader产生的事件就可以被我们的ClassPrinter消费了:
ClassPrinter cp = new ClassPrinter();
ClassReader cr = new ClassReader("java.lang.Runnable");
cr.accept(cp, 0);
上面的第二行代码创建了一个ClassReader来解析Runnable类。最后一行代码中的accept方法解析Runnable类的字节码,并且调用cp上对应的方法。结果如下:
java/lang/Runnable extends java/lang/Object {
run()V
}
注意,这里有多种方式来构造一个ClassReader的实例。可以通过类名,例如上面的例子,或者通过类的字节数组。或者类的输入流。类的输入流可以通过ClassLoader的getResourceAdStream方法:
cl.getResourceAsStream(classname.replace(’.’, ’/’) + ".class");

2.2.3生成类
生成一个类只需要ClassWriter组件即可。下面将使用一个例子来展示。考虑下面的接口:
package pkg;
public interface Comparable extends Mesurable {
int LESS = -1;
int EQUAL = 0;
int GREATER = 1;
int compareTo(Object o);
}
上面的类可以通过调用ClassVisitor的6个方法来生成:
ClassWriter cw = new ClassWriter(0);
cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
"pkg/Comparable", null, "java/lang/Object",
new String[] { "pkg/Mesurable" });
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I",
null, new Integer(-1)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",
null, new Integer(0)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",
null, new Integer(1)).visitEnd();
cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo",
"(Ljava/lang/Object;)I", null, null).visitEnd();
cw.visitEnd();
byte[] b = cw.toByteArray();
第一行代码用于创建一个ClassWriter实例,由它来构建类的字节数组(构造方法中的参数将在后面章节介绍)。
首先,通过调用visit方法来定义类的头部。其中,V1_5是一个预先定义的常量(与定义在ASM Opcodes接口中的其它常量一样),它定义了类的版本,Java 1.5.ACC_XX常量与java中的修饰符对应。在上面的代码中,我们指定了类是一个接口,因此它的修饰符是public和abstract(因为它不能实例化)。接下来的参数以内部名称形式定义了类的名称,(见2.1.2章节)。因为编译过的类中不包含package和import段,因此,类名必须使用全路径。接下来的参数与泛型对应(见4.1章节)。在上面的例子中,它的值为null,因为这个接口没有使用泛型。第五个参数指定了父类,也是以内部形式(接口隐式地继承自Object)。最后一个参数指定了该接口所继承的所有接口,该参数是一个数组。
接下来三次调用visitField方法,都是用来定义接口中三个字段的。visitField方法的第一个参数是描述字段的访问修饰符。在这里,我们指定这些字段为public,final和static。第二个参数是字段的名称,与在源代码中的名称一样。第三个参数,以类型描述符的形式指定了字段的类型。上面的字段是int类型,因此它的类型描述符是I。第四个参数与该字段的泛型对应,在这里为空,因为这个字段没有使用泛型。最后一个参数是这些字段的常量值,这个参数只能针对常量字段使用,如final static类型的字段。对于其他字段,它必须为空。因为这里没有使用注解,所以没有调用任何visitAnnotation和visitAttribute方法,而是直接调用返回的FieldVisitor的visitEnd方法。
visitMethod方法是用来定义compareTo方法的。该方法的第一个参数也是定义访问修饰符的,第二个参数是方法的名称,在源代码中指定的。第三个参数是该方法的描述符,第三个参数对应泛型,这里仍然为空,因为没有使用泛型。最后一个参数是指定该方法所声明的异常类型数组,在这个方法中为null,因为compareTo方法没有声明任何异常。visitMethod方法返回一个MethodVisitor(参见图3.4),它可以用来定义方法的注解和属性,以及方法的代码。在这里没有注解,因为这个方法是抽象的,因此我们直接调用了MethodVisitor的visitEnd方法。
最后,调用ClassWriter的visitEnd方法来通过cw类已经完成,然后调用toByteArray方法,返回该类的字节数组形式。
使用生成的类
前面获取的字节数组可以保存到Comparable.class文件中,以便在以后使用。此外它也可以被ClassLoader动态加载。可以通过继承ClassLoader,并重写该类的defineClass方法来实现自己的ClassLoader:
class MyClassLoader extends ClassLoader {
public Class defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
}
然后可以通过下面的代码来加载:
Class c = myClassLoader.defineClass("pkg.Comparable", b);
另一种加载生成的类的方法是通过定义一个ClassLoader的子类,并重写其中的findClass方法来生成需要的类:
class StubClassLoader extends ClassLoader {
@Override
protected Class findClass(String name)
throws ClassNotFoundException {
if (name.endsWith("_Stub")) {
ClassWriter cw = new ClassWriter(0);
...
byte[] b = cw.toByteArray();
return defineClass(name, b, 0, b.length);
}
return super.findClass(name);
}
}
实际上使用生成类的方式取决于使用的上下文,它超出了ASM API的范围。如果你打算写一个编译器,那么类的生成过程将被一个即将被编译的类的抽象的语法树驱动,并且生成的类将被保存到磁盘上。如果你打算编写一个动态的类代理生成工具或者在面向切面编程中使用,那么选择ClassLoader比较合适。
2.2.4转换类
到目前为止,ClassReader和ClassWriter都是独立使用。手工产生事件,然后被ClassWriter直接消费,或者对称地,事件由ClassReader产生,然后手工地消费,如通过一个自定义的ClassVisitor来实现。当把这些组件组合在一起使用时,将变得很有趣。第一步,将ClassReader产生的事件导入到ClassWriter,结果就是类将被ClassReader解析,然后再由ClassWriter重组为Class。
byte[] b1 = ...;
ClassWriter cw = new ClassWriter();
ClassReader cr = new ClassReader(b1);
cr.accept(cw, 0);
byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1
当然,有趣的并不是这个过程本身(因为有更简单的方式来复制一个字节数组)。但是,接下来介绍的ClassAdapter,它处于ClassReader和ClassWriter之间,将会带来变化:
byte[] b1 = ...;
ClasssWriter cw = new ClassWriter();
ClassAdapter ca = new ClassAdapter(cw); // ca forwards all events to cw
ClassReader cr = new ClassReader(b1);
cr.accept(ca, 0);
byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1
与上面代码对应的结构图如如2.6.在下面的图中,组件以方形表示,事件以箭头表示(在序列图中是一个垂直的时间线)。
图2.6 转换链

ASM3.0学习(二)-LMLPHP
执行结果没有任何变化,因为这里使用的ClassAdapter 事件过滤器没有过滤任何东西。但是,现在可以重写这个类来过滤一些事件,以实现转换类。例如,考虑下面这个ClassAdapter的子类:
public class ChangeVersionAdapter extends ClassAdapter {
public ChangeVersionAdapter(ClassVisitor cv) {
super(cv);
}
@Override
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
cv.visit(V1_5, access, name, signature, superName, interfaces);
}
}
这个类仅重写了ClassAdapter的一个方法。因此,所有的调用都未经过改变直接传递给了ClassVisitor实例cv,cv通过构造方法传递给自定义的ClassAdapter,除了visit方法,visit方法修改了类的版本号。对应的序列图如下:
图2.7

ASM3.0学习(二)-LMLPHP
可以通过修改visit方法的其它参数来实现其它转换,而不仅仅是修改类的版本号。例如,你可以给类增加一个借口。当然也可以修改类的名称,但是这需要修改很多东西,而不只是修改visit方法中类的名称。实际上,类名可能在很多地方存在,所有这些出现的地方都需要修改。
优化
前面的转换只改变了原始类中的四个字节。尽管如此,通过上面的代码,b1被完整的解析,产生的事件被用来从头构造b2,尽管这样做不高效。另一种高效的方式是直接复制不需要转换的部分到b2,这样就不需要解析这部分同时也不产生对应的事件。ASM会自动地对下面的方法进行优化:
 如果ClassReader检测到一个MethodVisitor直接被ClassVisitor返回,而这个ClassVisitor(如ClassWriter)是通过accept的参数直接传递给ClassReader,这就意味着这个方法的内容将不会被转换,并且对应用程序也是不可见的。
 在上面的情形中,ClassReader组件不会解析这个方法的内容,也不会产生对应的事件,而只是在ClassWriter中复制该方法的字节数组。
这个优化由ClassReader和ClassWriter来执行,如果它们拥有彼此的引用,就像下面的代码:
byte[] b1 = ...
ClassReader cr = new ClassReader(b1);
ClassWriter cw = new ClassWriter(cr, 0);
ChangeVersionAdapter ca = new ChangeVersionAdapter(cw);
cr.accept(ca, 0);
byte[] b2 = cw.toByteArray();
经过优化,上面的代码将比前面例子中的代码快两倍。因为ChangeVersionAdapter没有转换任何方法。对于转换部分或者所有方法而言,这种对速度的提高虽然很小,但确实显著的,可以达到10%到20%。不幸地是,这种优化需要复制在原始类中定义的所有常量到转换后的类中。这对于在转换中增加字段,方法或者指令什么的不是一个问题,但是相对于未优化的情形,这会导致在大的类转换过程中删除或者重命名很多类的元素。因此,这种优化适合于
需要添加代码的转换。
使用转换后的类
转换后的类b2可以保存到磁盘或者被ClassLoader加载,如前面章节描述的。但是在一个ClassLoader中只能转换被该ClassLoader加载的类。如果你想转换所有的类,你需要把转换的代码放置到一个ClassFileTransformer中,该类定义在java.lang.instrment包中(可以参看该报的文档获得详细信息):
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();
}
});

2.2.5移除类成员
前面例子中用来修改类版本号的方法也可以用在ClassVisitor接口中的其它方法上。例如,通过修改visitField和visitMethod方法中的access核name,你可以修改一个字段或者方法的访问修饰符和名称。更进一步,除了转发修改该参数的方法调用,你也可以选择不转发该方法调用,这样做的效果就是,对应的类元素将被移除。
例如,下面的类适配器将移除外部类和内部类,同时移除源文件的名称(修改过的类仍然是功能完整的,因为这些元素仅用作调试)。这主要是通过保留visit方法为空来实现。
public class RemoveDebugAdapter extends ClassAdapter {
public RemoveDebugAdapter(ClassVisitor cv) {
super(cv);
}
@Override
public void visitSource(String source, String debug) {
}
@Override
public void visitOuterClass(String owner, String name, String desc) {
}
@Override
public void visitInnerClass(String name, String outerName,
String innerName, int access) {
}
}
上面的策略对字段和方法不起作用,因为visitField和visitMethod方法必须返回一个结果。为了移除一个字段或者一个方法,你不能转发方法调用,而是返回一个null。下面的例子,移除一个指定了方法名和修饰符的方法(单独的方法名是不足以确定一个方法,因为一个类可以包含多个相同方法名的但是参数个数不同的方法):
public class RemoveMethodAdapter extends ClassAdapter {
private String mName;
private String mDesc;
public RemoveMethodAdapter(
ClassVisitor cv, String mName, String mDesc) {
super(cv);
this.mName = mName;
this.mDesc = mDesc;
}
@Override
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
if (name.equals(mName) && desc.equals(mDesc)) {
// do not delegate to next visitor -> this removes the method
return null;
}
return cv.visitMethod(access, name, desc, signature, exceptions);
}
}
2.2.6增加类成员
除了传递较少的方法调用,你也可以传递更多的方法调用,这样可以实现增加类元素。新的方法调用可以插入到原始方法调用之间,同时visitXxx方法调用的顺序必须保持一致(参看2.2.1)。
例如,如果你想给类增加一个字段,你需要在原始方法调用之间插入一个visitField调用,并且你需要将这个新的调用放置到类适配器的其中一个visit方法之中(这里的visit是指以visit打头的方法)。你不能在方法名为visit的方法中这样做,因为这样会导致后续对visitSource,visitOuterClass,visitAnnotation或者visitAttribute方法的调用,这样做是无效的。同样,你也不能将对visitField方法的调用放置到visitSource,visitOuterClass,visitAnnotation或者visitAttribute方法中。可能的位置是visitInnerClass,visitField,visitMethod和visitEnd方法。
如果你将这个调用放置到visitEnd中,字段总会被添加,除非你添加了显示的条件,因为这个方法总是会被调用。如果你把它放置到visitField或者visitMethod中,将会添加好几个字段,因为对原始类中每个字段或者方法的调用都会导致添加一个字段。两种方案都能实现,如何使用取决于你的需要。例如,你恶意增加一个单独的counter字段,用来统计对某个对象的调用次数,或者针对每个方法,添加一个字段,来分别统计对每个方法的调用。
注意:事实上,添加成员的唯一正确的方法是在visitEnd方法中增加额外的调用。同时,一个类不能包含重复的成员,而确保新添加的字段是唯一的方法就是比较它和已经存在的成员,这只能在所有成员都被访问之后来操作,例如在visitEnd方法中。程序员一般不大可能会使用自动生成的名字,如_counter$或者_4B7F_可以避免出现重复的成员,这样就不需要在visitEnd中添加它们。注意,如在第一章中讲的,tree API就不会存在这样的限制,使用tree API就可以在转换的任何时间点添加新成员。
为了展示上面的讨论,下面是一个类适配器,用来给一个类增加一个字段,除非这个字段已经存在:
public class AddFieldAdapter extends ClassAdapter {
private int fAcc;
private String fName;
private String fDesc;
private boolean isFieldPresent;
public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName,
String fDesc) {
super(cv);
this.fAcc = fAcc;
this.fName = fName;
this.fDesc = fDesc;
}
@Override
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value) {
if (name.equals(fName)) {
isFieldPresent = true;
}
return cv.visitField(access, name, desc, signature, value);
}
@Override
public void visitEnd() {
if (!isFieldPresent) {
FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);
if (fv != null) {
fv.visitEnd();
}
}
cv.visitEnd();
}
}
这个字段是在visitEnd方法中添加的。重写visitField方法不是为了修改已经存在的字段,而是为了检测我们希望添加的字段是否已经存在。注意,在调用fv.visitEnd之前,我们测试了fv是否为空,如我们前面所讲,一个class visitor的visitField方法可以返回null。
2.2.6转换链
到目前为止,我们看到了一些有ClassReader,一个类适配器和ClassWriter组成的转换链。当然,也可以将多个类适配器连接在一起,来实现更复杂的转换链。链接多个类适配器运行你组合多个独立的类转换,以实现更复杂的转换。注意,一个转换链条没必要是线性的,你可以编写一个ClassVisitor,然后同时转发所有的方法调用给多个ClassVisitor:
public class MultiClassAdapter implements ClassVisitor {
protected ClassVisitor[] cvs;
public MultiClassAdapter(ClassVisitor[] cvs) {
this.cvs = cvs;
}
@Override public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
for (ClassVisitor cv : cvs) {
cv.visit(version, access, name, signature, superName, interfaces);
}
}
...
}
相对地,多个类适配器也可以将方法调用都委托给相同的ClassVisitor(这需要额外的小心,以确保visit和visitEnd方法只被调用一次)。如图2.8这样的转换链也是可能地。

ASM3.0学习(二)-LMLPHP

01-25 16:43