2.2 ASM-类-接口和组件
本文可转载演绎,但需要注明原作者和本文链接。
2.2 接口和组件
2.2.1 介绍
ASM API对编译类进行生成和编辑,都是基于抽象类ClassVisitor实现的(参照表格 2.4)。
该类中的每一个方法都对应class文件中的同名的结构部分(参考表格-2.1:编译后的class结构)。
简单的结构部分可以通过一个方法进行方法,该方法参数描述了该结构部分,返回void。
其他可能是任意长度和复杂性的结构部分,可以通过调用一个初始化方法,返回一个辅助的visitor类。
这便是visitAnnotation、visitField、和visitMethod的调用模式,这几个方法分别返回AnnotationVisitor、FieldVisitor和MethodVisitor。
同样的原则也适用于递归调用这些辅助类。例如每个方法在抽象类FieldVisitor中都对应了class文件中同名的子结构。
图 2.4 :ClassVisitor类
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556 public abstract class ClassVisitor {protected final int api;protected ClassVisitor cv;public ClassVisitor(final int api) {//....}public ClassVisitor(final int api, final ClassVisitor cv) {//....}public void visit(int version, int access, String name, String signature,String superName, String[] interfaces) {//....}public void visitSource(String source, String debug) {//....}public void visitOuterClass(String owner, String name, String desc) {//....}public AnnotationVisitor visitAnnotation(String desc, boolean visible) {//....}public AnnotationVisitor visitTypeAnnotation(int typeRef,TypePath typePath, String desc, boolean visible) {//....}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) {//....}public MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions) {//....}public void visitEnd() {//....}}
在ClassVisitor类中的visitAnnotation方法会返回辅助类AnnotationVisitor。
在下一个章节会介绍如何创建和使用这些辅助类:在本章节将介绍一些可以单独使用ClassVisitor解决的简单问题。
图表-2.5.: FieldVisitor类
1234567 public abstract class FieldVisitor {public FieldVisitor(int api);public FieldVisitor(int api, FieldVisitor fv);public AnnotationVisitor visitAnnotation(String desc, boolean visible);public void visitAttribute(Attribute attr);public void visitEnd();}
ClassVisitor类的方法必须按照以下的顺序被调用执行。
visit方法必须被最先调用,接着再最多调用一次visitSource方法,接着再最多调用一次visitOuterClass方法,
接着再按照任意次序调用任意次的visitAnnotation和visitAttribute方法,
接着再按照任意次序调用任意次的visitInnerClass、visitField和visitMethod方法,最后调用一次visitEnd方法。
ASM提供了三个基于ClassVisitor的组件进行生成和转换class:
- ClassReader可以从byte数组中解析一个编译后的class,将一个ClassVisitor的实例作为accept的方法参数传递给ClassReader,
然后调用ClassVisitor的响应visitXxx方法。ClassReader可以被作为基于event**模式的生产者。 - ClassWriter是抽象类ClassVisitor的子类,它可以直接以二进制的方法构建编译后的class。ClassWriter可以看作是基于event模式的消费者。
- ClassVisitor可以完全代理其他ClassVisitor的方法调用。ClassVisitor可被看作是基于event模式的过滤器。
接下来的部分,是一些具体示例,演示使用这些组件如何构建和转换class。
2.2.2 解析Class
解析一个已经存在的class,所需要的唯一组件就是ClassReader类。举个例子介绍一下ClassReader类。
假设我们要打印一个class的内容,类似javap。
第一步要写一个ClassVisitor的子类来访问需要打印的信息。下面是一种方式,过于简单的实现:
第二步是绑定ClassPrinter和ClassReader,这样ClassReader产生的event都可以被ClassPrinter消费:
第而行语句创建了一个解析java.lang.Runnable接口的ClassReader。最后一行调用accept方法,解析Runnable接口并且调用对象cp中ClassVisitor的响应方法。
以下是程序输出结果:
ClassReader构造实例的方法有很多种。
像上面通过class全名调用的方式必须确保class文件能被访问到,或者通过一个代表class实体信息的byte数组或者InputSteam构造实例。
读取一个class的输入流,可以通过调用ClassLoader的getResourceAsStream方法:
2.2.3 生成Class
生成一个class所需要的唯一组件就是ClassWriter。举个例子说明一下。参考下面接口:
可以通过调用ClassVisitor的6个方法来生成这个接口:
第一行代码创建了一个ClassWriter实例,该实例将会生成代表class的byte数组。(构造参数会在下一章介绍。)
调用visit方法定义class的头信息。V1_5参数是一个常量,和其他ASM常量一样,在Opcode接口中定义。
代码指定了class的JVM版本为1.5。ACC_XXX常量标志对应了Java修饰符。
这里我们制定了该class是一个接口,有用public和abstract修饰符(因为该类不可以被实例化)。
第三个参数“pkg/Comparable”指定了class的名称,使用内部名格式(参照2.1.2章节)。
回顾一下,编译后的class,不包含package和import部分,所以所有的的class名称都必须是全名。
第四个参数是泛型(参照4.1章)。在这个示例中是null,因为接口没有参数化的类型变量。
第五个参数是父类,使用内部名格式(接口类隐式继承Object类)。
最后一个参数是一个数组表示该类实现的接口,使用内部名格式。
接下来调用的三个visiteField方法是类似的,用于定义接口的三个属性。
第一个参数设置了属性相应的修饰符。这里我们指定了该属性的修饰符为public、final、static。
第二个参数是属性的名称,和源码中是一样的。第三个参数制定了属性的类型,使用类型描述符的方式。
这里指定的属性是int类型的,所以类型描述符是I。
第四个参数是泛型。在这个示例中是null,因为我们没有使用泛型。
最后一个参数是该属性的常量值:这个参数只有在属性是恒定常量的时候才会被使用,即静态final属性。对于其他非静态常量,该参数必须为null。
因为这里没有使用注解,所以我们直接调用visitEnd方法,放回FieldVisitor,即不调用visitAnnotation、visitAttribute方法。
调用visitMethod方法定义compareTo方法。这里第一个参数同样是设置方法的修饰符。
第二个参数指定了方法的名称,和源码中的一样。第三个是该方法的描述符。第四个参数对应方法的泛型。在我们的示例中是null,因为定义的方法没有使用泛型。
最后一个参数指定了可能被该方法抛出的exception数组,使用内部名格式。这里是null,因为该方法不抛出任何异常。
visitMethod方法返回一个MethodVisitor实例(参考图标3.4),可以使用它定义方法的注解、属性和最重要的方法代码。
由于本方法不包含注解,并且该方法是abstract的,我们直接调用visitEnd方法。
最后我们调用cw的visitEnd方法结束该class的声明,并调用toByteArray**方法把该class输出成一个byte数组。
使用生成的类
上面生成的byte数组可以保存到Comparable.class文件中,以便后续使用。
另外,该数组也可以被ClassLoader动态加载。
定义一个ClassLoader的子类,指定它的defineClass方法为public:
|
|
生成的class可以通过以下方式被直接加载:
|
|
另一个方法加载生成的class,需要定义ClassLoader的子类,并重写findClass方法,动态生成需要的class:
现实情况中,如何使用生成的class取决于程序的上下文,已经超出了ASM API的介绍范围。
如果你写了一个编译器,类生成过程会被抽象语法树驱动来表示编译程序,生成的class会被保存在硬盘上。
如果你编写一个动态代理类生成器或者aspect weaver(切面织入器),需要使用ClassLoader。
2.2.4 转换生成的class
至此ClassReader和ClassWriter组件都是被单独使用的。
Event都是由ClassWriter自己产生并且直接消费,或者由ClassReader自己产生并且直接消费,即通过自定义的ClassVisitor实现。
当把这些组件整合起来使用的时候,事情就会变得非常有趣。第一步直接由一个ClassReader生成event传递给一个ClassWriter。
结果就是被ClassReader解析的class,被ClassWriter重建了:
这个例子实际上并不有趣(更加简单的实现方法是直接拷贝byte数组!),但请等等。
下一步是介绍在class的读取和写出的过程中使用ClassVisitor组件:
上面的代码所对应的结构展示在图表2.6中,组件使用方形表示,event使用箭头表示(垂直方向的时间线作为序列图)。
结果没有任何改变,因为ClassVisitor的event过滤器并没有做任何过滤操作。
但是现在通过重写一些方法,已经足够过滤一些event了,从而改变一个class。
例如,参考下面的ClassVisitor子类:
上面的class仅仅重写了ClassVisitor的一个方法。
其产生的结果是,所有通过调用构造函数传递进来的ClassVisitor对象cv,在调用了visit方法后,都会被修改class的版本号,然后才传递下去。
相应的序列图见图表2.7:
可以通过修改visit方法的其他参数来更改class,不仅仅改变class的版本号。
例如,可以添加一个接口到该类的接口列表。
也可以更改class的名字,但要实现修改class的名字,除了修改visit方法的参数,还要做很多其他的调整。
事实上,class的名字可能出现在编译类的很多不同地方,所有出现的地方都要修改成class的真实名字。
优化
上一部分的修改,仅仅改变了原class的4个字节。
然而,在上述代码中,b1数组被全部解析并产生了相应的event,用于从头构造b2数组,这样效率是非常低的。
高效的做法是直接将b1数组中不需要作改变的部分直接拷贝到b2数组中,对这部分不进行解些和生成event。
ASM会自动为方法执行这些优化:
- 当ClassReader检测到ClassVisitor中返回的MethodVisitor作为参数传递给一个ClassWriter对象,这意味着这个方法没有被修改,并且实际上不应该被应用所见。
- 这种情况下,ClassReader组件不解些方法的内容,也不生成相应的event,仅仅把表示该方法的byte数组拷贝到ClassWriter中。
当ClassReader和ClassWriter相互引用对方的时候,该优化会被执行,可以这样设置:
优化过的代码比原本的代码要快2倍,因为ChangeVersionAdapter不修改任意方法。
对于一边的class转换,修改一些或者所有方法,性能提升比较小,但是仍旧很明显:实际在10%到20%。
不幸的是,需要拷贝原class中定义的所有常量到转换的class中。(TODO FIXME 整个一段需要优化范围。)
对于添加属性、方法和指令的转换是没有问题的,但是这将产生更大的class文件,相比较未优化的情况,或对于删除或者重命名class中的元素。
因此,建议这种优化手段仅在“添加剂”转化中使用。
使用转换后的class
在上一个部分,我们介绍了转换后的class b2 可以被存储在硬盘上,或者被一个ClassLoader类加载。
但是在一个ClassLoader中转换后的class,既能够被本ClassLoader加载。
如果你想转换所有ClassLoader中的class,就需要在转换放在java.lang.instrument包中定义的ClassFileTransformer类中(了解更多信息请阅读该包的文档信息):
2.2.5 删除class的成员
上一部分改变类版本的方法,可以用在ClassVisitor的其他方法上。
例如修改方法中代表修饰符或者名称的参数,就可以修改相应方法或者属性的修饰符或者名称。
此外,除了转发修改后方法参数,也可选择不转发该调用。
产生的效果是,相应那个的class成员会被删除。
例如,下面的class适配器,删除了该类的内部类和外部类信息,以及编译该类的源文件名称信息
(生成的class仍然保留了完整的功能,因为删除的元素仅用于调试)。
这是通过不转发相应的visit方法实现的:
|
|
这种策略并不适用于属性和方法,因为visitField和visitMethod方法必须返回一个结果。
为了删除属性或者方法,就不能转发方法调用,直接返回null就可以了。
例如,下面的class适配器,通过指定方法的名称和描述符(名称不足以标识唯一一个方法,因为一个类可以包含很多名称相同,但参数类型不同的方法)删除了一个方法:
|
|
2.2.6 添加class成员
相比于减少转发你收到的调用,你可以多几次转发,来增加class的成员。
新的调用可以在原方法调用中的任何地方调用,需要确保不同visitXxx方法按照顺序被调用(参照章节2.2.1):
例如,如果你想在class中新增一个属性,你需要在原本的方法调用中插入一个visitField方法调用,并且在class适配器中的访问方法中插入该调用。
不可以在visit方法中插入visitField方法,这样将会导致visitField方法在visitSource、visitOuterClass、visitAnnotation、visitAttribute这些方法之前被调用,这是不合法的。
同样,也不能在visitSource、visitOuterClass、visitAnnotation、visitAttribute这些方法中调用visitField方法。
只可以在visitInnerClass、visitField、visitMethod、visitEnd方法中调用。
如果将新的调用方法添加在visitEnd方法中,这个属性肯定会被添加(除非你设置了其他条件来执行该调用),因为visitEnd方法总会被调用。
如果将调用放在visitField或visitMethod方法中,会添加几个属性:原class中每一个属性和方法都会调用该方法并增加一个属性。
这两种解决方案都是有意义的;使用那种取决于场景需求。例如你可以增加一个单一的计数器属性来统计对象的调用次数,或者每个方法的计数器来单独统计每个方法的调用次数。
备注
现实中真正正确的解决方案是在visitEnd方法中调用增加新成员的方法。
一个类不能包含重复的成员,确保新增成员唯一性,需要和所有的已有成员做比较,这只能在所有的已有成员都被访问过后才可以做比较,即在visitEnd方法中比较。
这是一个强约束。使用生成的名称可能和程序员使用的命名方式不同,例如在实践中使用’_counter$‘后者’_4B7F‘这种命名可以防止类成员重复,这样就不必在visitEnd方法中调用了。
需要注意的是,正如第一章介绍的,Tree API的调用是没有限制的:在类转换的过程中可以使用API添加类成员。
为了说明上面的讨论,下面有一个class适配器,会在class中添加一个属性,出发该属性已经在该class中:
属性在visitEnd方法中添加。重写了visitField方法,并不是为了修改或删除原有的属性,而是检查需要添加的属性是否已经存在。
注意一下,visitEnd方法中,在调用fv.visitEnd()方法前,调用的fv != null 判断语句:原因之前章节有介绍,一个class的visitor调用visitField方法,可能返回null。
2.2.7 转换链路
至此为止,我们已经了解了由ClassReader、class适配器和ClassWriter组成的转换链。
当然也可以由多个class适配器组成更加复杂的转换链,。
链路中的几个适配器可以撰写几个不同的类转换器,来实现复杂的转换工作。
需要知道的是,转换链不必是线性的。
可以实现一个ClassVisitor,同时转发它所接受的所有方法调用到不同的ClassVisitor:
|
|
对等的多个class适配器可以委托给同一个ClassVisitor(这需要一些预防措施,比如ClassVisitor的visit方法和visitEnd方法只能被调用一次)。
因此一个像图标2.8展示的转换链才是完全有可能的。