ASM3.0学习(二)

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");java

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 转换链程序员


执行结果没有任何变化,由于这里使用的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编程


能够经过修改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这样的转换链也是可能地。ide

相关文章
相关标签/搜索