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 转换链
执行结果没有任何变化,因为这里使用的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方法修改了类的版本号。对应的序列图如下:
可以通过修改visit方法的其它参数来实现其它转换,而不仅仅是修改类的版本号。例如,你可以给类增加一个借口。当然也可以修改类的名称,但是这需要修改很多东西,而不只是修改visit方法中类的名称。实际上,类名可能在很多地方存在,所有这些出现的地方都需要修改。
优化
前面的转换只改变了原始类中的四个字节。尽管如此,通过上面的代码,b1被完整的解析,产生的事件被用来从头构造b2,尽管这样做不高效。另一种高效的方式是直接复制不需要转换的部分到b2,这样就不需要解析这部分同时也不产生对应的事件。ASM会自动地对下面的方法进行优化:
这个优化由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();
}
});
}