ASM分析系列之二: Core API-Classes

目录

 

一、Classes

    本章主要解释如何使用core ASM API生成和转换编译后的Java类文件。本章先展示了编译后类文件的结构,然后讲解了对应的生成和转换类文件的ASM接口、组件和工具,同时给出了很多具有说明性的例子。下一章主要对方法的注解和泛型进行详述。

1.1 结构

1.1.1 概览

 编译类的总体结构非常简单。实际上,与简单编译的应用程序不同,编译类保留了结构信息和源代码中几乎所有的符号。事实上,一个已编译的类包含:

  1. 修饰符部分(例如 public和private),名称,父类,接口和类的注解

  2. 类中的成员部分。描述了成员的修饰符、名称、类型和字段注解

  3. 类中声明的方法和构造函数部分。描述了修饰符、名称、返回参数类型和方法的注解。同时也包含了以Java字节码指令序列的形式展示的方法中的代码块。

 

    源码和编译后的类文件存在以下区别:

  1. 编译后的类文件仅仅只描述一个类,然而源码文件可能会包含多个类。例如,一个源码文件在一个类中声明了一个内部类,编译后会生成两个类文件:一个描述主类,一个描述内部类。然而主类的文件中会包含它的内部classes引用,以及在方法内部定义的内部类会包含对其封闭方法的引用。

  2. 编译后的类文件不包含注释,但是可以包含类、字段、方法和代码属性,这些属性可用于将附加信息与这些元素关联。java5中引入注解后,属性就变得没什么用处了。

  3. 编译后的类不包含package和import部分,因此类型名称需要用全路径指定。

    另一个非常重要的结构上的区别是一个编译后的类文件包含常量池部分。这个常量池是类中出现的所有数字、字符串和类型常量。这些常量只会在常量池中定义一次,并在类文件中根据它们的下标被引用。ASM后续会隐藏常量池的细节,用户不需要再考虑它。下图总结了编译后类文件的总体结构。详细的结构在Java虚拟机规范第4节中进行了描述。

z+IzAcy0lHIWwAAAABJRU5ErkJggg==

另一个重要的区别是在编译后的文件和源文件中Java类型的展现形式不同。下一张会对它们在编译后类文件中的展示进行详述。

1.1.2 内部名称

在许多情况,类型被限制为类或是接口类型。例如类的超类,类实现的接口或方法抛出的异常不能使基础类型或数组类型,必须是类或者接口。这些类型在class文件中用内部名称标识。类的内部名称只是类的

完全限定名称。其中的点被斜杠替换。例如,String的内部名称是java/lang/String。

1.1.3 类型描述符

内部名称internal names只用于限定在类或是接口类型的场景下。在所有其他情况下(如字段类型),Java类型都用类型描述符的编译类表示。

image.png

原始类型的描述符是单个字符:Z对应boolean,C对应char,B对应byte,S代表short,I代表int,F代表float,J代表long,D代表Double。类类型的描述符是该类的内部名称,前面是L,后面是一个分号。例如,字符串的类型描述符是Ljava/lang/String;最后,数组类型的描述符是方括号,后面跟着数组元素类型的描述符。

1.1.4 方法描述符

方法描述符是一个类型描述符的列表,它描述了方法的参数类型和返回类型。一个方法描述符以左括号开头,后面是每个形式参数的类型描述符,后面是右括号,后面是返回类型的类型描述符,如果方法返回void,则是V(方法描述符不包含方法的名称或参数名称)。下图是几个方法描述的例子。

image.png

一旦你知道类型描述符的工作方式,就更容易理解方法的描述符。例如 (I)I 描述了一个接受int类型参数的方法,其返回值也是int类型。

 

1.2 接口和组件

1.2.1 Presentation

ASM中用于生成和转换class文件的API是基于一个ClassVisitor抽象类的,如下图。这个类中的每个方法都对应class文件结构中的同样名称元素。简单的部分使用一个方法调用来访问,它的参数描述它们的内

容,然后返void。内容长度和复杂性没有限制的部分可以通过以初始入口方法访问并返回辅助的visitor 类。例如visitAnnotation,visitField和visitMethod等方法,它们会分别返回 AnnotationVisitor, FieldVisitor 和 MethodVisitor .

image.pngimage.png

同样的原则在辅助类中递归使用。例如FieldVisitor抽象类中的每个方法对应了同名的类文件子结构,visitAnnotation方法返回一个辅助的AnnotationVisitor。辅助visitor的创建和使用在下一章中详述:本章重点

关注ClassVisitor类中的独立的简单问题。

ClassVisitor类中的方法必须按照以下顺序调用,和Javadoc中的指定的顺序一致。

image.png

这意味着必须首先调用visit,然后最多调用一次visitSource,然后最多调用一次visitOuterClass,然后跟随任意多次任意顺序的visitAnnotation 和visitAttribute 方法,然后跟随任意多次任意顺序的

visitInnerClass, visitField 和 visitMethod ,最终使用对visitEnd方法的单次调用结束。

ASM基于ClassVisitor API提供了三个核心组件来生成和转换class文件:

  1. ClassReader类解析以字节数组形式给出的编译类,并将其作为参数传递给ClassVisitor类中的visitxxx方法。它可以被视为事件生产者。

  2. ClassWriter类是ClassVisitor抽象类的一个子类,它用来直接以二进制形式构建编译后的类文件。它输出一个包含编译后class文件的字节数组,可以通过toByteArray方法来进行检索,它可以认为是一个消费者。

  3. ClassVisitor类将其接收到的所有方法委托给另一个ClassVisitor实例。它可以被视为事件过滤器。

 

1.2.2 解析类

唯一用来解析已存在类的组件是ClassReader组件。让我们来举个例子说明这一点。建设我们希望以类似于javap工具的方式来打印类的二进制内容,第一步是编写ClassVisitor类的子类,该子类打印它访问的类信息。这里有一个简化的实现。

 

image.png

image.png

第二步是将ClassPrinter和ClassReader组件结合起来,这样ClassReader产生的事件就可以被我们的ClassPrinter消费了。

image.png

上图中的第二行代码创建了一个ClassReader类来解析Runnble类。最后一行调用的accept方法解析Runnable类的字节码并调用了对应的ClassVisitor方法。输出结果如下图所示:

image.png

注意,有几种方法可以构造ClassReader实例。需要进行解析的类可以按上图中的名称或是字节数组或是InputStream来指定。可以通过ClassLoader的getResourceStream方法来读取类文件的输入流,如下

image.png

1.2.3 生成类

生成类所需的惟一组件是ClassWriter component。让我们举个例子来说明这一点。考虑下面的接口:

image.png

它可以通过调用ClassVisitor的六个方法来生成:

image.png

第一行创建了一个ClassWriter实例,用来实际构建标识类的字节数组(构造参数将在下一张详述)。

对visit方法的调用定义了这个类的header。V1_5参数是一个常量定义,类似其他ASM常量一样,存放在ASM Opcodes接口中。它指定了类的版本,Java 1.5。ACC_XX常量代表Java的修饰符。这里我们制定了

这个类是一个接口,并且它是public和abstract的(因为它不能被实例化,在源文件中对接口来说是一个冗余修饰符)。下一个参数制定了类的名字,以一个内部名称格式,即完全限定名称。回想一下,class文件没有package或是import部分,所有的类名都必须完全限定。下一个参数对应泛型(后面章节会讲到)。在我们的例子中水口的,因为这个接口不吉首类型变量作为参数。第五个参数制定了父类,也是以完全限定形式指定(接口隐式继承自Object)。最后一个参数是扩展的接口数组,由它们的内部名称指定。(???)

下面三个对visitField函数的调用是类似的,用来定义接口的三个成员。第一个参数对应Java限定符,这里我们指定这些成员的限定符为 public, final 和 static。第二个参数是成员的名称,和它在

源码中的展示一致。第三个参数是成员的类型,以前面讲过的类型描述符形式。这里的成员类型为int,其描述符为I。第四个参数对应泛型。在我们的例子中为null因为此处成员类型没有使用泛型。最后一个参数是成员的常量值,这个参数只能对常量成员使用,如,final static 成员。对于其他的成员必须为空。由于此处没有注释,我们直接调用返回的FieldVisitor中的visitEnd方法,不在调用其他的visitiAnnotation或是visitAttribute方法。

visitMethod方法用来定义源文件中的compareTo方法。这第一个参数同样对应Java中的修饰符,第二个参数是方法名称,和源码中保持一致,第三个参数是方法描述符。第四个参数是泛型。在

此处为空。最后一个参数是可能被方法跑出的异常数组,异常类型由全限定符制定。这里为空,因为该方法不会抛出异常。visitMethod方法放回了一个MethodVisitor,可以用来定义方法的注解和属性,以及最重要的方法代码块。在此处由于方法没有注解以及是抽象方法,我们直接调用返回的MethodVisitor的visitEnd方法。

最后,visitEnd方法的调用用来通知cw类定义已经完成,并调用toByteArray来以字节数组的形式检索它。

 

1.2.3.1 使用生成的类

前面生成的字节数组被存放在Comparable.class文件中以供后续使用。它可以被动态加载到ClassLoader中。其中一种方法是定义一个ClassLoader的子类,它的defineClass方法是pulic的。

image.png

然后生成的类就可以被直接加载进来了

image.png

另外一种更清晰的加载生成雷的方法是定义一个ClassLoader的子类,重写它的findClass方法来在查找的过程中动态生成类(???why cleaner???)

image.png

实际上使用生成雷的方法取决于应用上下文,不属于ASM API的范畴。如果你在编写一个编译器,类生成过程将由要编译的程序的抽象语法树驱动,生成的类将存储在磁盘上。如果您正在编

写动态代理类生成器或aspect weaver,您将以某种方式使用类加载器。

1.2.4 转换类

到目前为止ClassReader和ClassWriter组件都是单独使用的。事件是手动产生并且直接被ClassWriter消费,或是相反,ClassReader产生并手动消费。例如,被一个ClassVisitor消费。当这些组件一起使用时事情会变得很有趣。第一步是将一个ClassReader生成的事件指向一个ClassWriter。结果是被ClassReader解析的类会被ClassWriter重新组装。

image.png

这本身并不是很有趣(有更简单的方法来复制一个字节数组!),但是等等。下一步是在ClassReader和ClassWriter之间添加一个ClassVisitor:

image.png

上面代码的体系结构如下图所示

image.png

然而,由于ClassVisitor的时间过滤器没有过滤任何东西,最终的结果不变。但是现在,通过覆盖一些方法来过滤一些事件,以便能够转换类就足够了。例如,考虑下面的ClassVisitor子类:

image.png

这个方法重写了ClassVisitor类中的唯一一个方法。结果是,除构造请求外的所有请求都会在修改类版本号后原样转发到ClassVisitor中。时序图如下

image.png

通过修改visit方法的其他参数,你能不仅仅实现修改类版本号的其他转换操作。例如,你可以给接口实现类列表中添加接口。也可以修改类名,但是这不仅仅只要求在visit方法中修改name参数。

实际上类名可能会在编译后的calss文件的许多地方出现,所有出现的地方都必须更改为对应的重命名。

1.2.4.1 优化

前面转化的例子值改变了原始class文件中的四个字节。然而原class文件被全部解析了一遍转移到新的class中,效率不是很高。可以使用效率更高的方法将原class中的不需要进行转换的部分直接拷贝到新的class中区。ASM自动执行了以下的优化方法:

  1. 如果一个ClassReader组件检测到ClassVisitor返回的MethodVisitor作为一个参数传递到ClassWriter的accept方法中去了,这意味着这个方法的内容不会被修改,甚至不会被应用感知。

  2. 在这种情况下ClassReader组件不会解析该方法的内容,不会生成对应的event,而是直接拷贝字节数组到ClassWriter中去。

这个优化策略是在ClassReader和ClassWriter互相引用时才会执行的,可以参考下面的例子

image.png

多亏了这个优化策略,上面的代码比之前的代码效率提升了两倍,因为ChangeVersionAdapter不会转换任何方法。

在更通用的类转换场景中,可能会对部分或是全部方法进行转换,加速效果会小一些,但也是可感知的:提升了10%~20%的效率。不幸的是,这个优化策略要求拷贝原始class中的所有常量到新的class中区。这对于增加成员,方法或指令来说不是问题,但是对于移除或是重命名许多类元素的场景来说,和非优化方法对比导致类文件变大。因此推荐只在做加法的转换操作中使用优化策略。

1.2.4.2 使用转换后的类

转换后的class可以在磁盘中存储或是通过ClassLoader加载,如上一章所述。但是在类加载器中的转换操作只能转换已经被这个ClassLoader所加载过的类。如果你希望转换所有的类,就需要把你的转换过程放在一个ClassFileTransformer中,如java.lang.instrument包中定义的那样:

image.png

1.2.5 移除类成员

在前面章节中介绍的转换类版本号的办法也可以在ClassVisitor类的其他方法中使用。例如,通过在visitField和visitMethod方法中改变access或是name,可以实现更改成员或是方法的限定符。此外,你可以选择不使用修改参数转发方法,而是直接不转发整个方法调用。这样做的结果是对应类中的元素被移除了。

例如,下面的类适配器将删除关于外部类和内部类的信息,以及编译类的源文件的名称(生成的类仍然是全功能的,因为这些elements仅用于调试目的)。这是通过在适当的访问方法中不转发任何东西来完成的:

image.png

这个策略不适用于对成员和方法的操作,因为visitField和visitMethod必须返回一个结果。为了实现移除成员或是方法,你必须不转发方法调用请求,并且返回null给调用器。如下例所示,calss adapter通过制定方法名称和修饰器移除一个独立的方法(名称不足以标识一个方法,因为一个类可能存在多个参数不同名称相同的重载方法)

image.png

image.png

1.2.6 添加类成员

除了转发比接收到的请求更少的调用,你还可以转发更多调用,其结果即为增加了类元素。可以在原始方法调用之间的几个位置插入新的调用,前提是必须遵守各种visitXxx方法的调用顺序(参见2.2.1节)。

例如,如果你想在勒种新增一个成员field,你需要在原始的方法调用中新增一个对visitField的调用,并且必须将这个新调用放入类适配器的访问方法之一。例如,在visit方法中不能这样做,因为这可能导致调用visitField,然后是visitSource、visitOuterClass、visitAnnotation或visitAttribute,这是无效的。出于同样的原因,您不能将这个新调用放在visitSource、visitOuterClass、visitAnnotation或visitAttribute方法中。唯一的可能是visitInnerClass、visitField、visitMethod或visitEnd方法。

如果把新的调用放在visitEnd方法中,这个成员总会被添加进去(除非额外增加了制定条件),因为这个方法总是会被调用。如果放在visitField或是visitMethod方法中,将添加几个字段:原始类中的每个字段或方法一个。两种解决办法都是有效的,这取决于你的需求。例如,您可以添加一个计数器字段来计数对象上的调用,或者每个方法上的一个计数器来分别计数每个方法的调用。

注意,实际上,唯一真正正确的解决方案是通过在visitEnd方法中发出附加调用来添加新成员。实际上,一个类不能包含重复的成员,确保新成员是重复的唯一方法是将它与所有现有成员进行比较,只有在所有成员都被访问过之后才能进行比较,即在visitEnd方法中。这种用法限制性较高。不太可能使用程序员指定生成的名称,如_counter$或_4B7F_,在实践中足以避免重复的成员,而不必在visitEnd中添加它们。请注意,正如第一章中所讨论的,tree API没有这个限制:可以在任何时候使用这个API在转换中添加新成员。

下面是一个新增类成员的例子

image.png

这个成员是在visitEnd方法中被添加的。visitField方法重写不是因为要修改已存在的成员或是移除成员,只是为了检测成员名称是否重复。注意在visitEnd方法中调用fv.visitEnd方法之前的fv!=null的判断,这是因为在前面章节里我们看到过在visitField方法里返回null的例子。

1.2.7 转换流程

到目前为止我们看到了一个通过ClassReader,ClassAdapter和ClassWriter组成的简单class转换的流程。当然,我们可以使用更复杂的流程,将多个class的adapter流程整合到一起。链接多个适配器允许我们来讲几个独立的类转换为一个大类来完成复杂的转换操作。注意,一个转换流程没必要是线性的。你可以让一个ClassVisitor将他接收到的方法调用请求同时转发到多个ClassVisitor中。

image.png

对称地,几个类适配器可以委托给同一个类访问器(这需要一些预防措施,例如,确保在这个类访问器上调用visit和visitEnd方法一次)。因此,像图2.8所示那样的转换链是完全可能的。

image.png

 

1.3 工具

作为对ClassVisitor和相关的ClassReader、ClassWriter的补充,ASM提供了org.objectweb.asm.util包,其中的几个工具在类生成器和是适配器中较为有用,但是在运行时不需要。ASM同时提供了一个用于在运行时操纵内部名称、类型描述符和方法描述符的实用程序类。所有这些工具如本章的细节所示。

1.3.1 类型

如前面章节所示,ASM API以Java类型在class中的格式一样对类型进行操作,如内部名称或是类型描述符。同样,也可以像源码中的形式一样对其进行操作,使代码可读性更高。但是这要求ClassReader和ClassWriter中的对称转换。这就是ASM不透明将内部名称和描述符转换为源码中同等形式的原因。但是ASM也提供了Type类以在必要的场景进行手动操作。

Type对象代表了Java中的对象,并且可以从类型描述符或类对象构造。同时,Type类中还包含了一些表示原始类型的静态变量,例如Type.INT_TYPE对应int类型。

1.3.2 类遍历Visitor

1.3.3 CheckClassAdapter

1.3.4 ASMifier

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值