asm4-guide-英文.pdf
asm4-guide-中文
还是人家官网文档写得好,什么快速入门都不如官方文档,阅读两小时,就知道怎么回事了。
ASM使用指南中文版
1. 介绍
1.1. 动机
程序分析、程序生成和程序转换都是非常有用的技术,可在许多应用环境下使用:
- 程序分析,既可能只是简单的语法分析(syntaxic parsing),也可能是完整的语义分 析 (sematic analysis),可用于查找应用程序中的潜在 bug、检测未被用到的代码、 对代码 实施逆向工程,等等。
- 程序生成,在编译器中使用。这些编译器不仅包括传统编译器,还包括用于分布式程序 设计的 stub 编译器或 skeleton 编译器,以及 JIT(即时)编译器,等等。
- 程序转换可,用于优化或混淆(obfuscate)程序、向应用程序中插入调试或性能监视 代 码,用于面向方面的程序设计,等等。
所有这些技术都可针对任意程序设计语言使用,但对于不同语言,其使用的难易程度可能会 有 所不同。对于 Java 语言,它们可用于 Java 源代码或编译后的 Java 类。在使用经过编译的类时, 其好处之一
显然就是不需要源代码。因此,程序转换可用于任何应用程序,既包括保密的源代码, 也包含商业应用程序。使用已编译类的另一个好处
是,有可能在运行时,在马上就要将类加载到Java 虚拟机之前,对类进行分析、生成或转换(在运行时生成和编译源代码也可以,但其速度很 慢,而且需要一个完整的 java 编译器)。其好处是,诸如 stub 编译器或方面编织器等工具对用户变为透明。
由于程序分析、生成和转换技术的用途众多,所以人们针对许多语言实现了许多用于分析、 生 成和转换程序的工具,这些语言中就包括 Java 在内。ASM 就是为 Java 语言设计的工具之一, 用 于进行运行时(也是脱机的)类生成与转换。于是,人们设计了 ASM1库,用于处理经过编译 的 Java 类。这个库的设计使其尽可能保持快速和小型化。对于那些在运行时使用 ASM 进行动态 类生成或转换的应用程序来说,尽可能提高库的运行速度是非常重要的,这样可以保证这些应 用 程序的速度不致下降过多。而保持 ASM 库的小型化也非常重要,一方面是为了在内存有 限的环 境中使用,另一方面,也为了避免使那些使用 ASM 的小型应用程序或库增大过多。
ASM 并不是惟一可生成和转换已编译 Java 类的工具,但它是最新、最高效的工具之一,可 从 http://asm.objectweb.org 下载。其主要优点如下:
1 ASM 的名字没有任何含义:它只是引用 C 语言中的__asm__关键字,这个关键字允许执行一些用汇 编 语言编写的函数。
- 有一个简单的模块 API,设计完善、使用方便。
- 文档齐全,拥有一个相关的 Eclipse 插件。
- 支持最新的 Java 版本——Java 7。 小而快、非常可靠。
- 拥有庞大的用户社区,可以为新用户提供支 持。
- 源许可开放,几乎允许任意使用。
1.2. 概述
1.2.1. 范围
ASM 库的目的是生成、转换和分析以字节数组表示的已编译 Java 类(它们在磁盘中的存储 和 在 Java 虚拟机中的加载都采用这种字节数组形式)。为此,ASM 提供了一些工具,使用高于字节级别的概念来读写和转换这种字节数组,这些概念包括数值常数、字符串、Java 标识符、Java 类型、Java 类结构元素,等等。注意,ASM 库的范围严格限制于类的读、写、转换和分析
。具体来说,类的加载过程就超出了它的范围之外
。
1.2.2. 模型
ASM 库提供了两个用于生成和转换已编译类的 API,一个是核心 API
,以基于事件
的形式来表示类,另一个是树 API
,以基于对象的形式
来表示类。
在采用基于事件的模型时,类是用一系列事件来表示的,每个事件表示类的一个元素,比 如 它的一个标头、一个字段、一个方法声明、一条指令,等等。基于事件的 API 定义了一组 可能 事件,以及这些事件必须遵循的发生顺序,还提供了一个类分析器,为每个被分析元素生 成一个 事件,还提供一个类写入器,由这些事件的序列生成经过编译的类。
而在采用基于对象的模型时,类用一个对象树表示,每个对象表示类的一部分,比如类本身、 一个字段、一个方法、一条指令,等等,每个对象都有一些引用,指向表示其组成部分的对象。 基 于对象的 API 提供了一种方法,可以将表示一个类的事件序列转换为表示同一个类的对象树, 也 可以反过来,将对象树表示为等价的事件序列。换言之,基于对象的 API 构建在基于事件的API 之上。
这两个 API 可以与“用于 XML 的简单 API”(Simple API for XML,SAX)和用于 XML 文 档的“文档对象模型(Document Object Model,DOM)API”相比较:基于事件的 API 类似于 SAX,而基于对象的 API 类似于 DOM。基于对象的 API 构建在基于事件的 API
之上,类似于 DOM 可在 SAX 的上层提供。
ASM 之所以要提供两个 API,是因为没有哪种 API 是最佳的。实际上,每个 API 都有自己 的优缺点:
- 基于事件的 API 要快于基于对象的 API,所需要的内存也较少,因为它不需要在内 存中 创建和存储用于表示类的对象树(SAX 与 DOM 之间也有同样的差异)。
- 但在使用基于事件的 API 时,类转换的实现可能要更难一些,因为在任意给定时 刻, 类中只有一个元素可供使用(也就是与当前事件对应的元素),而在使用基于对 象的 API 时,可以在内存中获得整个类。
注意,这两个 API 都是仅能同时维护一个类,而且独立于其他类,也就是说,它们不会维 护有关类层级结构的信息,如果类的转换影响到其他类,那其他这些类的修改应当由用户负责完 成。
1.2.3. 体系结构
ASM 应用程序拥有一个很强壮的体系结构方面(aspect)。事实上,对于基于事件的 API, 其组织结构是围绕事件生成器(类分析器)、事件使用器(类写入器)和各种预定义的事件筛选 器
进行的,在这一结构中可以添加用户定义的生成器、使用器和筛选器。因此,这一 API 的使 用分为两个步骤:
- 将事件生成器、筛选器和使用器组件组装为可能很复杂的体系结构。
- 然后启动事件生成器,以执行生成或转换过程。
基于对象的 API 也有一个体系结构方面:实际上,用于操作类树的类生成器或转换器组 件 是可以组成形成的,它们之间的链接代表着转换的顺序。
尽管典型 ASM 应用程序中的大多数组件体系结构都非常简单,但还是可以想象一下类似于 如 下所示的复杂体系结构,其中的箭头表示在类分析器、写入器或转换器之间进行的基于事件或 基于 对象的通信,在整个链中的任何位置,都可能会在基于事件与基于对象的表示之间进行转换:
1.3. 组织
ASM 库划分为几个包,以几个 jar 文件的形式进行分发:
-
org.objectweb.asm
和org.objectweb.asm.signature
包定义了基于事件的
API,并提供了类分析器和写入器组件。它们包含在asm.jar
中。 -
org.objectweb.asm.util
包,位于asm-util.jar
中,提供各种基于
核心 API 的工具,可以在开发和调试 ASM 应用程序时使用。 -
org.objectweb.asm.commons
包提供了几个很有用的预定义类转换器,它们大 多 是基于核心 API 的。这个包包含在asm-commons.jar
中。 -
org.objectweb.asm.tree
包,位于asm-tree.jar
存档文件中,定义了基于对 象的 API,并提供了一些工具,用于在基于事件和基于对象的表示方法之间进行转换。 -
org.objectweb.asm.tree.analysis
包提供了一个类分析框架和几个预定义的 类 分析器,它们以树 API 为基础。这个包包含在asm-analysis.jar
文件中。
本文档分为两部分。第一部分介绍核心 API
,即 asm、asm-util 和 asm-commons 存档文 件。第二部分介绍树 API
,即 asm-tree 和 asm-analysis 存档文件。每部分至少包含该 API 与类相关的一章内容、该 API 与方法相关的一章内容、该 API 与注释、泛型等相关的一章内容。 每章都会介绍编程接口及相关的工具与预定义组件。所有示例的源代码都可以从 ASM 网站上获得。
这种组织形式便于循序渐进地介绍类文件特征,但有时需要将同一个 ASM 类的介绍分散 到 几节中。因此,建议依次阅读本文档。如需有关 ASM API 的参考手册,请使用 Javadoc。
印刷约定
斜体 用于强调句子中的元素。
一般字体显示 用于表示代码段。
加粗字体用于强调代码元素。
斜体加粗字体用于表示标记和代码中的变量部分。
1.4. 致谢
感谢 François Horn 在制作本文档期间提供的宝贵评论,这些意见极大地提升了本文档的 结构和可读性。
第一部分 核心 API
本章说明如何使用核心 ASM API 来生成和转换经过编译的 Java 类。首先介绍已编译类,然 后将利用大量说明性示例,介绍用于生成和转换已编译类的相应 ASM 接口、组件和工具。方法、 注释和泛型的内容将在之后各章中说明
2. class 类
2.1.结构体
2.1.1. 概述
已编译类的总体结构非常简单。实际上,与原生编译应用程序不同,已编译类中保留了来自 源代码的结构信息和几乎所有符号。事实上,已编译类中包含如下各部分:
- 专门一部分,描述类的修饰符(比如 public 和 private)、名字、超类、接口和 注 释。
- 类中声明的每个字段各有一部分。每一部分描述一个字段的修饰符、名字、类型和注释。
- 类中声明的每个方法及构造器各有一部分。每一部分描述一个方法的修饰符、名字、返回类型与参数类型、注释。 它还以 Java 字节代码指令的形式,包含了该方法的已编 译 代码。
但在源文件类和已编译类之间还是有一些差异:
源文件的类和编译类,结构会有编码。比如对于类内变量的使用,会变成this.
- 一个已编译类仅描述一个类,而一个源文件中可以包含几个类。比如,一个源文件描 述 了一个类,这个类又有一个内部类,那这个源文件会被编译为两个类文件 : 主类 和内 部类各一个文件。但是,主类文件中包含对其内部类的引用,定义了内部方法 中定义的 类会包含引用,引向其封装的方法。
- 已编译类中当然不包含注释(comment),但可以包含类、字段、方法和代码属性,可 以 利用这些属性为相应元素关联更多信息。Java 5 中引入可用于同一目的的注释
(annotaion)以后,属性已经变得没有什么用处了。 - 编译类中不包含 package 和 import 部分,因此,所有类型名字都必须是完全限定的。
另一个非常重要的结构性差异是已编译类中包含常量池(constant pool
)部分。这个池是一个数组,其中包含了在类中出现的所有数值、字符串和类型常量。这些常量仅在这个常量池部 分 中定义一次,然后可以利用其索引,在类文件中的所有其他各部分进行引用。幸好,ASM 隐藏 了与常量池有关的所有细节,所以我们不用再为它操心了。图 2.1 中总结了一个已编译 类的整体 结构。其确切结构在《Java 虚拟机规范》第 4 节中描述。
Modifiers, name, super class, interfaces | |
Constant pool: numeric, string and type constants Source file name (optional) | |
Enclosing class reference | |
Annotation* | |
Attribute* | |
Inner class* | Name |
Field* | Modifiers, name, type |
Annotation* | |
Attribute* | |
Method* | Modifiers, name, return and parameter types |
Annotation* | |
Attribute* | |
Compiled code |
*图 2‐1 已编译类的整体结构(表示零个或多个)
另一个重要的差别是 Java 类型在已编译类和源文件类中的表示不同。后面几节将解释它 们 在已编译类中的表示。
2.1.2. 内部名
在许多情况下,一种类型只能是类或接口类型。例如,一个类的超类、由一个类实现的接 口, 或者由一个方法抛出的异常就不能是基元类型或数组类型,必须是类或接口类型。这些类 型在已 编译类中用内部名字表示。一个类的内部名就是这个类的完全限定名,其中的点号用斜 线代替。 例如,String 的内部名为 java/lang/String。
2.1.3. 类型描述符
内部名只能用于类或接口类型。所有其他 Java 类型,比如字段类型,在已编译类中都是用
类型描述符表示的(见图 2.2)。
Java 类型 | 类型描述符 |
---|---|
boolean | Z |
char | C |
byte | B |
short | S |
int | I |
float | F |
long | J |
double | D |
Object | Ljava/lang/Object; |
int[] | [I |
Object[][] | [[Ljava/lang/Object; |
图 2‐2 一些 Java 类型的类型描述符
基本类型的描述符是单个字符: Z 表示 boolean,C 表示 char,B 表示 byte,S 表示 short, I 表示 int,F 表示 float,J 表示 long,D 表示 double。一个类类型的描述符 是这个类的 内部名, 前面加上字符 L , 后面跟有一个分号。例如, String 的类型描述符为 Ljava/lang/String;。而一个数组类型的描述符是一个方括号后面跟有该数组元素类型的描 述符。
2.1.4. 方法描述符
方法描述符是一个类型描述符列表,它用一个字符串描述一个方法的参数类型和返回类型。 方
法描述符以左括号开头,然后是每个形参的类型描述符,然后是一个右括号,接下来是返回类 型的
类型描述符,如果该方法返回 void,则是 V(方法描述符中不包含方法的名字或参数名)。
源文件中的方法声明 | 方法描述符 |
---|---|
void m(int i, float f) | (IF)V |
int m(Object o) | (Ljava/lang/Object;)I |
int[] m(int i, String s) | (ILjava/lang/String;)[I |
Object m(int[] i) | ([I)Ljava/lang/Object; |
图 2.3 方法描述符举例
一旦知道了类型描述符如何工作,方法描述符的理解就容易了。例如,(I)I 描述一个方 法, 它接受一个 int 类型的参数,返回一个 int。图 2.3 给出了几个方法描述符示例。
2.2. 接口和组件
2.2.1. 介绍
用于生成和变转已编译类的 ASM API 是基于 ClassVisitor 抽象类的(见图 2.4)。这 个 类中的每个方法都对应于同名的类文件结构部分(见图 2.1)。简单的部分只需一个方法调 用就能 访问,这个调用返回 void,其参数描述了这些部分的内容。有些部分的内容可以达到 任意长度、 任意复杂度,这样的部分可以用一个初始方法调用来访问,返回一个辅助的访问者 类。 visitAnnotation、visitField 和 visitMethod 方法就是这种情况,它们分别返 回 AnnotationVisitor、FieldVisitor 和 MethodVisitor.
public abstract class ClassVisitor{
public ClassVisitor(int api);
public ClassVisitor(int api, 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);
AnnotationVisitor visitAnnotation(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);
void visitEnd();
}
图 2.4 ClassVisitor 类
针对这些辅助类递归适用同样的原则。例如,FieldVisitor 抽象类中的每个方法(见图2.5)对应于同名的类文件子结构,visitAnnotation 返回一个辅助的 AnnotationVisitor, 和在 ClassVisitor 中一样。这些辅助访问者类的创建和使用在随后几章中解释:实际上, 本 章仅限于只需 ClassVisitor 类本身就能解决的简单问题。
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();
}
图 2.5 FieldVisitor 类
ClassVisitor 类的方法必须按以下顺序调用(在这个类的Javadoc 中规定):
visit visitSource? visitOuterClass?
( visitAnnotation| visitAttribute )*
( visitInnerClass | visitField |visitMethod )* visitEnd
这意味着必须首先调用 visit,然后是对 visitSource 的最多一个调用,接下来是对 visitOuterClass 的最多一个调用 , 然后是可按任意顺序对 visitAnnotation 和 visitAttribute 的任意多个访问 , 接下来是可按任意顺序对 visitInnerClass 、 visitField 和 visitMethod 的任意多个调用,最后以一个 visitEnd 调用结束。
ASM 提供了三个基于 ClassVisitor API 的核心组件,用于生成和变化类:
- ClassReader 类分析以字节数组形式给出的已编译类,并针对在其 accept 方法 参数 中传送的 ClassVisitor 实例,调用相应的 visitXxx 方法。这个类可以看 作一个事 件产生器。
- ClassWriter 类是 ClassVisitor 抽象类的一个子类,它直接以二进制形式生成 编 译后的类。它会生成一个字节数组形式的输出, 其中包含了已编译类, 可以用 toByteArray 方法来提取。这个类可以看作一个事件使用器。
- ClassVisitor 类将它收到的所有方法调用都委托给另一个 ClassVisitor 类。 这个 类可以看作一个事件筛选器。
接下来的各节将用一些具体示例来说明如何使用这些组件来生成和转换类。
2.2.2. 解析类
在分析一个已经存在的类时,惟一必需的组件是 ClassReader 组件。让我们用一个例子 来 说明。假设希望打印一个类的内容,其方式类似于 javap 工具。第一步是编写 ClassVisitor 类的一个子类,打印它所访问的类的相关信息。下面是一种可能的实现方式,它有些过于简化了:
public class ClassPrinter extends ClassVisitor {
public ClassPrinter() {
super(ASM4);
}
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 调用相应的 ClassVisitor 方法。结果为以下输出:
// 真实的Runnable接口也只有一个方法 void run(),和这个输出对应
java/lang/Runnable extends java/lang/Object
{
run()V}
注意,构建 ClassReader 实例的方式有若干种。必须读取的类可以像上面一样用名字指定, 也 可 以 像 字 母 数 组 或 InputStream 一 样 用 值 来 指 定 。 利 用 ClassLoader 的getResourceAsStream 方法,可以获得一个读取类内容的输入流,如下:
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 进行六次方法调用来生成它:
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 参数1
是一个常数,与所有其他 ASM 常量一样,在 ASM Opcodes 接口中定义。它指明了类的版本——Java 1.5。ACC_XXX 常量2
是与 Java 修饰 符对 应的标志。这里规定这个类是一个接口,而且它是 public 和 abstract 的(因为它不能 被实例化)。下一个3
参数以内部形式规定了类的名字(见 2.1.2 节)。回忆一下,已编译类不包含 Package 和 Import
部分,因此,所有类名都必须是完全限定的。下一个参数4
对应于泛型(见 4.1 节)。在我们的例子中,这个参数是 null,因为这个接口并没有由类型变量进行参数化。 第 五5
个参数是内部形式的超类(接口类隐式继承自 Object)。最后一个参数是一个数组,其 中是 被扩展的接口,这些接口由其内部名指定。
一个完整的类定义 public class test<File> extend Object implent A , B { }五个参数对应五个
接下来对 visitField 的三次调用是类似的,用于定义三个接口字段。第一个参数是一组 标志,对应于 Java 修饰符。这里规定这些字段是 public、final 和 static 的。第二个参数 是字段的名字,与它在源代码中的显示相同。第三个参数是字段的类型,采用类型描述符形式。 这 里,这些字段是 int 字段,它们的描述符是 I。第四个参数对应于泛型。在我们的例子中, 它是 null,因为这些字段类型没有使用泛型。最后一个参数是字段的常量值:这个参数必须仅用于真正的常量字段,也就是 final static 字段。对于其他字段,它必须为 null。由于此处 没有注释, 所以立即调用所返回的 FieldVisitor 的 visitEnd 方法, 即对其 visitAnnotation 或 visitAttribute 方法没有任何调用。
visitMethod 调用用于定义 compareTo 方法,同样,第一个参数是一组对应于 Java 修饰
符的标志。第二个参数是方法名,与其在源代码中的显示一样。第三个参数是方法的描述符。第 四 个参数对应于泛型。在我们的例子中,它是 null,因为这个方法没有使用泛型。最后一个参 数是 一个数组,其中包括可由该方法抛出的异常,这些异常由其内部名指明。它在这里为 null, 因为这个方法没有声明任何异常。visitMethod 方法返回 MethodVisitor(见图 3.4),可用 于定义该方法的注释和属性,最重要的是这个方法的代码。这里,由于没有注释,而且这个方法 是抽象的,所以我们立即调用所返回的 MethodVisitor 的 visitEnd 方法。
类方法或者filed等结束后还有注释,使用visitEnd代表着结束。
对 visitEnd 的最后一个调用是为了通知 **cw:**这个类已经结束,对 toByteArray 的调用用于以字节数组的形式提取它。
使用生成的类
前面的字节数组可以存储在一个 Comparable.class 文件中,供以后使用。或者,也可 以 用 ClassLoader 动态加载它。一种方法是定义一个 ClassLoader 子类,它的 defineClass 方法是公有的:
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。其结果是, 类编写器重新构建了由类读取器分析的类:
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader(b1); cr.accept(cw, 0);
byte[] b2 = cw.toByteArray(); // b2 和 b1 表示同一个类
这本身并没有什么真正的意义(还有其他更简单的方法可以用来复制一个字节数组!), 但等 一等。下一步是在类读取器和类写入器之间引入一个 ClassVisitor:
byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
// cv 将所有事件转发给 cw
ClassVisitor cv = new ClassVisitor(ASM4, cw) {
}; ClassReader cr = new ClassReader(b1); cr.accept(cv, 0);
byte[] b2 = cw.toByteArray(); // b2 与 b1 表示同一个类
图 2.6 给出了与上述代码相对应的体系结构,其中的组件用方框表示,事件用箭头表示(其
中的垂直时间线与程序图中一样)。
图 2.6 转换链
但结果并没有改变,因为 ClassVisitor 事件筛选器没有筛选任何东西。但现在,为了能 够转换一个类,只需重写一些方法,筛选一些事件就足够了。例如,考虑下面的 ClassVisitor 子类:
public class ChangeVersionAdapter extends ClassVisitor {
public ChangeVersionAdapter(ClassVisitor cv) {
super(ASM4, 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);
} }
这个类仅重写了 ClassVisitor 类的一个方法。结果,所有调用都被不加改变地转发到传 送给构造器的类访问器 cv,只有对 visit 方法的调用除外,在转发它时,对类版本号进行了修改。相应的程序图在图 2.7 中给出。
图 2.7 ChangeVersionAdapter 的程序图
通过修改 visit 方法的其他参数,可以实现其他转换,而不仅仅是修改类的版本。例如, 可以向实现接口的列表中添加一个接口。还可以改变类的名字,但进行这种改变所需要做的工作 要多得多,不只是改变 visit 方法的 name 参数了。实际上,类的名字可以出现在一个已编译 类的许多不同地方,要真正实现类的重命名,必须修改类中出现的所有这些类名字。
改进
前面的转换只修改了原类的四个字节。但是,在使用上面的代码时,整个 b1 均被分析,并利用相应的事件从头从头构建了 b2,这种做法的效率不是很高。如果将 b1 中不被转换的部分 直 接复制到 b2 中,不对其分析,也不生成相应的事件,其效率就会高得多。ASM 自动为方法 执行这一优化:
- 在 ClassReader 组件的 accept 方法参数中传送了 ClassVisitor , 如果 ClassReader 检测到这个 ClassVisitor 返回的 MethodVisitor 来自一个
ClassWriter,这意味着这个方法的内容将不会被转换,事实上,应用程序甚至不会 看到其内容。 - 在这种情况下,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 中执行的类转换只能转换由这个类加载器加载的类。如果希望转换所有类,则必 须将转换放在 ClassFileTransformer 内部,见 java.lang.instrument 包中的定义(更 多细节,请参阅这个软件包的文档):
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 参数,可以改变一个字段 或一个方 法的修饰字段或名字。另外,除了在转发的方法调用中使用经过修改的参数之外,还 可以选择根 本不转发该调用。其效果就是相应的类元素被移除
。
例如,下面的类适配器移除了有关外部类及内部类的信息,还删除了一个源文件的名字,也
就是由其编译这个类的源文件(所得到的类仍然具有全部功能,因为删除的这些元素仅用于调 试 目的)。这一移除操作是通过在适当的访问方法中不转发任何内容而实现的:
public class RemoveDebugAdapter extends ClassVisitor {
public RemoveDebugAdapter(ClassVisitor cv) {
super(ASM4, 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 ClassVisitor {
private String mName;
private String mDesc;
public RemoveMethodAdapter( ClassVisitor cv, String mName, String mDesc) {
super(ASM4, 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)) {
// 不要委托至下一个访问器 -> 这样将移除该方
return null;
}
return cv.visitMethod(access, name, desc, signature, exceptions);
}
}
2.2.6. 添加类成员
上述讨论的是少转发一些收到的调用,我们还可以多“转发”一些调用,也就是发出的调用 数多于收到的调用,其效果就是增加了类成员。新的调用可以插在原方法调用之间的若干位置, 只要遵守各个 visitXxx 必须遵循的调用顺序即可(见 2.2.1 节)。
例如,如果要向一个类中添加一个字段,必须在原方法调用之间添加对 visitField 的一 个新调用,而且必须将这个新调用放在类适配器的一个访问方法中。比如,不能在 visit 方法 中 这样做, 因为这样可能会导致对 visitField 的调用之后跟有 visitSource 、 visitOuterClass、visitAnnotation 或 visitAttribute,这是无效的。出于同样的原 因,不能将这个新调用放在 visitSource、visitOuterClass、visitAnnotation 或 visitAttribute 方法中 . 仅有的可能位置是 visitInnerClass 、 visitField 、 visitMethod 或 visitEnd 方法。
如果将这个新调用放在 visitEnd 方法中,那这个字段将总会被添加(除非增加显式条件), 因为这个方法总会被调用。如果将它放在 visitField 或 visitMethod 中,将会添加几个字 段:原类中的每个字段和方法各有一个相应的字段。这两种解决方案都可能发挥应有的作用;具 体取决于你的需求。例如,可以仅添加一个计数器字段,用于计算对一个对象的调用次数,也可 以为每个方法添加一个计数器,用于分别计算对每个方法的调用次数。
注意
:事实上,惟一真正正确的解决方案是在 visitEnd 方法中添加更多调用,以添加新成员。实际上,
一个类中不得包含重复成员,要确保一个新成员没有重复成员,惟一方法就是将它与所有已有成员进行对 比,只有在 visitEnd 方法中访问了所有这些成员后才能完成这一工作。这种做法是相当受限制的。在
实践中,使用程序员不大可能使用的生成名
,比如_counter$或_4B7F_ i 就足以避免重复成员了, 并不需要将它们添加到 visitEnd 中。注意,在第一章曾经讨论过,树 API 没有这一限制:可以在任意 时刻向使用这个 API 的转换中添加新成员。
为了举例阐述以上讨论,下面给出一个类适配器,它会向类中添加一个字段,除非这个字段 已经存在:
public class AddFieldAdapter extends ClassVisitor {
private int fAcc;
private String fName;
private String fDesc;
private boolean isFieldPresent;
public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName, String fDesc) {
super(ASM4, 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);
// 如果返回null 意味着之前的方法被移除了
if (fv != null) {
fv.visitEnd();
}
}
cv.visitEnd();
}
}
这个字段被添加在 visitEnd 方法中。visitField 方法未被重写为修改已有字段或删 除 一个字段,只是检测一下我们希望添加的字段是否已经存在。注意 visitEnd 方法中在调 用 fv.visitEnd() 之前的 fv != null 检测:这是因为一个类访问器可以在 visitField 中返 回 null,在上一节已经看到这一点。
2.2.7. 转化链
到目前为止,我们已经看到一些由 ClassReader、类适配器和 ClassWriter 组成的简单 转换链。当然可以使用更为复杂的转换链,将几个类适配器链接在一起。将几个适配器链接在一 起,就可以组成几个独立的类转换,以完成复杂转换。还要注意,转换链不一定是线性的。我们可以编写一个 ClassVisitor,将接收到的所有方法调用同时转发给几个 ClassVisitor:
public class MultiClassAdapter extends ClassVisitor {
protected ClassVisitor[] cvs;
public MultiClassAdapter(ClassVisitor[] cvs) {
super(ASM4);
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 针对这个 ClassVisitor 恰好仅被调用一次)。因此,诸如图 2.8 所 示的这样一个转换链是完全可行的。
图 2.8 一个复杂转换
2.3. 工具
除了 ClassVisitor 类和相关的 ClassReader、ClassWriter 组件之外,ASM 还在 org.objectweb.asm.util
包中提供了几个工具,这些工具在开发类生成器或适配器时可能 非 常有用,但在运行时不需要它们。ASM 还提供了一个实用类,用于在运行时处理内部名、类 型描述符和方法描述符。所有这些工具都将在下面介绍。
2.3.1. 类型 Type
在前几节已经看到,ASM API 公开 Java 类型的形式就是它们在已编译类中的存储形式,也 就是说,作为内部特性或类型描述符。也可以按照它们在源代码中的形式来公开它们,使代码更 便 于阅读。但这样就需要在 ClassReader 和 ClassWriter 中的两种表示形式之间进行系统 转 换,从而使性能降低。这就是为什么 ASM 没有透明地将内部名和类型描述符转换为它们等价的源代码形式。但它提供了 Type 类,可以在必要时进行手动转换。
一个 Type 对象表示一种 Java 类型,既可以由类型描述符构造,也可以由 Class 对象构建。 Type 类还包含表示基元类型的静态变量。例如,Type.INT_TYPE 是表示 int 类型的 Type 对 象。
getInternalName 方 法 返 回 一 个 Type 的 内 部 名 。 例 如 ,Type.getType(String.class).getInternalName() 给出 String 类的内部名,即 “java/lang/String”。这一方法只能对类或接口类型使用。
getDescriptor 方法返回一个 Type 的描述符。比如 , 在代码中可以不使用 “Ljava/lang/String;” , 而 是 使 用 Type.getType(String.class). getDescriptor()。或者,可以不使用 I,而是使用 Type.INT_TYPE.getDescriptor()。
Type 对象还可以表示方法类型。这种对象既可以从一个方法描述符构建,也可以由 Method 对 象 构 建 。 getDescriptor 方 法 返 回 与 这 一 类 型 对 应 的 方 法 描 述 符 。 此 外 , getArgumentTypes 和 getReturnType 方法可用于获取与一个方法的参数类型和 返回类型 相对应的 Type 对象。例如,**Type.getArgumentTypes("(I)V")**返回一个仅有一个 元素 Type.INT_TYPE 的数组。与此类似 , 调用 Type.getReturnType("(I)V") 将返回 Type.VOID_TYPE 对象。
2.3.2 TraceClassVisitor
要确认所生成或转换后的类符合你的预期,ClassWriter 返回的字母数组并没有什么真正 的用处,因为它对人类来说是不可读的。如果有文本表示形式,那使用起来就容易多了。这正是 TraceClassVisitor 类提供的东西。从名字可以看出,这个类扩展了 ClassVisitor 类, 并生成所访问类的文本表示。因此, 我们不是用 ClassWriter 来生成类, 而是使用 TraceClassVisitor,以获得关于实际所生成内容的一个可读轨迹。甚至可以同时使用这两 者,这样要更好一些。除了其默认行为之外,TraceClassVisitor 实际上还可以将对其方 法 的所有调用委托给另一个访问器,比如 ClassWriter:
ClassWriter cw = new ClassWriter(0);
TraceClassVisitor cv = new TraceClassVisitor(cw, printWriter);
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();
这一代码创建了一个 TraceClassVisitor,将它自己接收到的所有调用都委托给 cw,然 后将这些调用的一份文本表示打印到 printWriter。例如,如果在 2.2.3 节的例子中使用 TraceClassVisitor,将会得出:
// 类版本号 49.0 (49)
// 访问标志 1537
public abstract interface pkg/Comparable implements pkg/Mesurable
{
// 访问标志 25
public final static I LESS = -1
//访问标志 25
public final static I EQUAL = 0 //访问标志 25
public final static I GREATER = 1 //访问标志 1025
public abstract compareTo(Ljava/lang/Object;)I
}
注意,可以在生成链或转换链的任意位置使用 TraceClassVisitor,以查看在链中这 一 点发生了什么,并非一定要恰好在 ClassWriter 之前使用。还要注意,有了这个适配器 生成 的类的文本表示形式,可能很轻松地用 String.equals() 来对比两个类。
2.3.3. CheckClassAdapter
ClassWriter 类并不会核实对其方法的调用顺序是否恰当,以及参数是否有效。因此,有可能会生成一些被 Java 虚拟机验证器拒绝的无效类。为了尽可能提前检测出部分此类错误,可 以使用 CheckClassAdapter 类。和 TraceClassVisitor 类似, 这个类也扩展了ClassVisitor 类,并将对其方法的所有调用都委托到另一个 ClassVisitor,比如一个 TraceClassVisitor 或一个 ClassWriter。但是,这个类并不会打印所访问类的文本表示, 而是验证其对方法的调用顺序是否适当,参数是否有效,然后才会委托给下一个访问器。当发 生 错误时,会抛出 IllegalStateException或IllegalArgumentException。
为核对一个类,打印这个类的文本表示形式,最终创建一个字节数组表示形式,应当使用类 似于如下代码:
ClassWriter cw = new ClassWriter(0);
TraceClassVisitor tcv = new TraceClassVisitor(cw, printWriter);
CheckClassAdapter cv = new CheckClassAdapter(tcv);
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();
注意,如果以不同顺序将这些类访问器链在一起,那它们执行的操作也将以不同顺序完成。 例如,利用以下代码,这些核对工作将在轨迹之后进行
ClassWriter cw = new ClassWriter(0);
CheckClassAdapter cca = new CheckClassAdapter(cw);
TraceClassVisitor cv = new TraceClassVisitor(cca, printWriter);
和使用 TraceClassVisitor 时一样,也可以在一个生成链或转换链的任意位置使用 CheckClassAdapter,以查看该链中这一点的类,而不一定只是恰好在 ClassWriter 之前使用。
2.3.4. ASMifier
这个类为 TraceClassVisitor 工具提供了一种替代后端(该工具在默认情况下使用Textifier 后端,生成如上所示类型的输出)。这个后端使 TraceClassVisitor 类的每个方法都会打印用于调用它的 Java 代码。例如,调用 **visitEnd()**方法将打印 cv.visitEnd();。
其结果是,当一个具有 ASMifier 后端的 TraceClassVisitor 访问器访问一个类时,它会打 印用 ASM 生成这个类的源代码
。如果用这个访问器来访问一个已经存在的类,那这一点是很有 用的。例如,如果你不知道如何用 ASM 生成某个已编译类,可以编写相应的源代码,用 javac 编译它,并用 ASMifier 来访问这个编译后的类。将会得到生成这个已编译类的 ASM 代码!
ASMifier 类也可以在命令行中使用。例如,使用以下命令。
java -classpath asm.jar:asm-util.jar \
org.objectweb.asm.util.ASMifier \
java.lang.Runnable
将会生成一些代码,经过缩进后,这些代码就是如下模样:
package asm.java.lang;
import org.objectweb.asm.*;
public class RunnableDump implements Opcodes {
public static byte[] dump() throws Exception {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
// 别忘了,这个viist就是生成方法 ClassWriter中有介绍
cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "java/lang/Runnable", null, "java/lang/Object", null);
{
mv = cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "run", "()V",null, null);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}
3. 方法
本章解释如何用核心 ASM API 生成和转换已编译方法。首先介绍编译后的方法,然后介
绍 用于生成和转换它们的相应 ASM 接口、组件和工具,并给出大量说明性示例。
3.1. 结构
在编译类的内部,方法的代码存储为一系列的字节码指令。为生成和转换类,最根本的就 是 要了解这些指令,并理解它们是如何工作的。本节将对这些指令进行全面概述
,这些内容足 以开 始编写简单的类生成器与转换器代码。如需完整定义,应当阅读 Java 虚拟机规范。
3.1.1. 执行模型
在介绍字节代码指令之前,有必要先来介绍 Java 虚拟机执行模型。我们知道,Java 代码 是 在线程内部执行的。每个线程都有自己的执行栈,栈由帧组成。每个帧表示一个方法调用: 每次 调用一个方法时,会将一个新帧压入当前线程的执行栈。当方法返回时,或者是正常返 回,或者 是因为异常返回,会将这个帧从执行栈中弹出,执行过程在发出调用的方法中继续进 行(这个方 法的帧现在位于栈的顶端)。
每一帧包括两部分:一个局部变量部分和一个操作数栈部分。局部变量
部分包含可根据索 引 以随机顺序访问的变量。由名字可以看出,操作数栈
部分是一个栈,其中包含了供字节代码 指令
用作操作数的值。这意味着这个栈中的值只能按照“后入先出”顺序访问。不要将操作数 栈和线 程的执行栈相混淆:执行栈中的每一帧都包含自己的操作数栈。
局部变量部分与操作数栈部分的大小取决于方法的代码。这一大小是在编译时计算的,并 随 字节代码指令一起存储在已编译类中。因此,对于对应于某一给定方法调用的所有帧,其局 部变 量与操作数栈部分的大小相同,但对应于不同方法的帧,这一大小可能不同。
图 3.1 一个具有 3 帧的执行栈
图 3.1 给出了一个具有 3 帧的示例执行栈。第一帧包含 3 个局部变量,其操作数栈的最大值 为 4,其中包含两个值。第二帧包含 2 个局部变量,操作数栈中有两个值。最后是第三帧,位 于执行栈的顶端,包含 4 个局部变量和两个操作数。
在创建一个帧时,会将其初始化,提供一个空栈,并用目标对象 this(对于非静态方 法) 及该方法的参数来初始化其局部变量。例如,调用方法 a.equals(b) 将创建一帧,它有 一个空 栈,前两个局部变量被初始化为 a 和 b(其他局部变量未被初始化)。
局部变量部分和操作数栈部分中的每个槽(slot)可以保存除 long 和 double 变量之外 的 任意 Java 值。long 和 double 变量需要两个槽。这使局部变量的管理变得复杂:例 如,第 i 个 方法参数不一定存储在局部变量 i 中。例如,调用 Math.max(1L, 2L) 创建一 个帧,1L 值位 于前两个局部变量槽中,值 2L 存储在第三和第四个槽中。
3.1.2. 字节码指令
字节代码指令由一个标识该指令的操作码和固定数目的参数组成:
- 操作码( opcode )是一个无符号字节(unsigned byte)值——即字节代码名,由助记符号标识。例如,操作码 0 用 助 记符号 NOP 表示,对应于不做任何操作的指令。
- 参数是静态值,确定了精确的指令行为。它们紧跟在操作码之后给出。比如 GOTO 标 记 指令(其操作码的值为 167)以一个指明下一条待执行指令的标记作为参数标 记。
不要 将指令参数与指令操作数相混淆:参数值是静态已知的,存储在编译后的 代码中,而 操作数值来自操作数栈,只有到运行时才能知道。
字节代码指令可以分为两类:
- 少部分指令,被设计用来在局部变量和操作数栈之间传送值;
- 其 他一些指令仅用于操作数栈:它们从栈中弹出一些值,根据这些值计算一个结果,并将它压回栈 中。
ILOAD, LLOAD, FLOAD, DLOAD 和 ALOAD 指令读取一个局部变量,并将它的值压到操 作数栈中。 它们的参数是必须读取的局部变量的索引 i。ILOAD 用于加载一个 boolean、byte、 char、short 或 int 局部变量。LLOAD、FLOAD 和 DLOAD 分别用于加载 long、float 或 double
值。(LLOAD 和 DLOAD 实际加载两个槽 i 和 i+1)。最后,ALOAD 用于加载任意非基本类型值,即对 象和数组引用。与之对应,ISTORE、LSTORE、FSTORE、DSTORE 和 ASTORE指 令从操作数栈 中弹出一个值,并将它存储在由其索引 i 指定的局部变量中。
可以看到,xLOAD 和 xSTORE 指令被赋入了类型(事实上,下面将要看出,几乎所有指令 都被赋予了类型)。它用于确保不会执行非法转换。实际上,将一个值存储在局部变量中,然后 再 以不同类型加载它,是非法的。例如,ISTORE 1 ALOAD 1 序列是非法的——它允许将一个 任 意内存位置存储在局部变量 1 中,并将这个地址转换为对象引用!但是,如果向一个局部变 量中存储一个值,而这个值的类型不同于该局部变量中存储的当前值,却是完全合法的。这意味着一个局部变量的类型,即这个局部变量中所存值的类型可以在方法执行期间发生变化。
上面已经说过,所有其他字节代码指令都仅对操作数栈有效。它们可以划分为以下类别(见 附件 A.1):
stack栈
这些指令用于处理栈上的值:POP 弹出栈顶部的值,DUP 压入顶部栈值的一个副本, SWAP 弹出两个值,并按逆序压入它们,等等。Constants常量
这些指令在操作数栈压入一个常量值:ACONST_NULL压入null,ICONST_0压入 int 值 0,FCONST_0 压入 0f,DCONST_0 压入 0d,BIPUSH b 压入字节值 b,SIPUSH s 压入 short 值 s,LDC cst 压入任意 int、float、long、double、String 或 class常量 cst,等等。Arithmetic and logic算术与逻辑
这些指令从操作数栈弹出数值,合并它们,并将结果压入栈中。它们没有任何 参数。xADD、xSUB、xMUL、xDIV 和 xREM 对应于**+、-、*、/和%运算,其中 x 为 I、 L、F 或 D 之一。类似地,还有其他对应于<<、>>、>>>、|、&和^**运算的指令,用于处理 int 和 long 值。Casts类型变换
这些指令从栈中弹出一个值,将其转换为另一类型,并将结果压入栈中。它们对
应于 Java 中的类型转换表达式。I2F, F2D, L2D 等将数值由一种数值类型转换为另一种类型。CHECKCAST t 将一个引用值转换为类型 t。Objects对象
这些指令用于创建对象、锁定它们、检测它们的类型,等等。例如,NEW type 指令将
一个 type 类型的新对象压入栈中(其中 type 是一个内部名)。Fields字段
这些指令读或写一个字段的值。GETFIELD owner name desc 弹出一个对象引用,并压入其 name 字段中的值。PUTFIELD owner name desc 弹出一个值和一个对象引用,并 将这个值存储在它的 name 字段中。在这两种情况下,该对象都必须是 owner 类型,它的字段必须为 desc 类型。GETSTATIC 和 PUTSTATIC 是类似指令,但用于静态字段。METHODS 方法
这些指令调用一个方法或一个构造器。它们弹出值的个数等于其方法参数个数加 1 (用于目标对象),并压回方法调用的结果。INVOKEVIRTUAL owner name desc 调用在 类 owner 中定义的 name 方法,其方法描述符为 desc。INVOKESTATIC 用于静态方法, INVOKESPECIAL 用于私有方法和构造器,INVOKEINTERFACE 用于接口中定义的方法。最后,对于 Java 7 中的类,INVOKEDYNAMIC 用于新动态方法调用机制。Arrays数组
这些指令用于读写数组中的值。xALOAD指令弹出一个索引和一个数组,并压入此索
引处数组元素的值。xASTORE 指令弹出一个值、一个索引和一个数组,并将这个值存储在该数组的这一索引处。这里的 x 可以是 I、L、F、D 或 A,还可以是 B、C 或 S。Jumps 跳转
这些指令无条件地或者在某一条件为真时跳转到一条任意指令。它们用于编译if、 for、do、while、break 和 continue 指令。例如,IFEQ label 从栈中弹出一个 int 值,如果这个值为 0,则跳转到由这个 label 指定的指令处(否则,正常执行下一条指令)。还有许多其他跳转指令,比如 IFNE 或 IFGE。最后,TABLESWITCH 和 1 对应于 identifier.class Java 语法。方法 LOOKUPSWITCH 对应于 switch Java 指令。Return 返回
最后,xRETURN 和 RETURN 指令用于终止一个方法的执行,并将其结果返回给调 用 者。RETURN 用于返回 void 的方法,xRETURN 用于其他方法。
3.1.3. 示例
让我们看一些基本示例,具体体会一下字节代码指令是如何工作的。考虑下面的 bean 类:
package pkg;
public class Bean {
private int f;
public int getF() {
return this.f;
}
public void setF(int f) {
this.f = f;
}
}
getter 方法的字节代码为
// 初始化话是局部变量栈第一个元素为this
// ALOAD 用于加载任意非基本类型值到操作数栈,即对 象和数组引用
// 0是局部变量的索引,就是第一个所以是this
// 取出局部变量栈,索引0,压入操作数栈
ALOAD 0
// GETFIELD owner name desc
// 弹出一个值和对象引用,并将值压入其 name 字段中的值
// 这里取出F压入栈中
GETFIELD pkg/Bean f I
// 弹出INT值,f
IRETURN
第一条指令读取局部变量 0(它在为这个方法调用创建帧期间被初始化为 this),并将这个 值压入操作数栈中。第二个指令从栈中弹出这个值,即 this,并将这个对象的 f 字段压入栈中,即 this.f。最后一条指令从栈中弹出这个值,并将其返回给调用者。图 3.2 中给出了这个方法 执行帧的持续状态。
图 3.2 getF 方法的持续帧状态:a) 初始状态,b) 在 ALOAD 0 之后,c) 在 GETFIELD 之后
setter 方法的字节代码:
// 初始化时,压入局部变量 f 与 this如下图 a
ALOAD 0
// 局部变量栈弹出第一个值-this,进入操作数栈 如 b
ILOAD 1
// 局部变量栈再弹出第二个值,进入操作数栈 如 c
PUTFIELD pkg/Bean f I
// PUTFIELD 弹出一个对象和引用,存储在name f中
RETURN
图 3.3 setF 方法的持续状态:a) 初始状态,b) 在 ALOAD 0 之后,c)在 ILOAD 1 之后,d) 在PUTFIELD 之后
和之前一样,第一条指令将 this 压入操作数栈。第二条指令压入局部变量 1,在为这个方 法调用创建帧期间,以 f 参数初始化该变量。第三条指令弹出这两个值,并将 int 值存储在被 引用对象的 f 字段中,即存储在 this.f 中。最后一条指令在源代码中是隐式的,但在编译后的代码中却是强制的,销毁当前执行帧,并返回调用者。这个方法执行帧的持续状态如图 3.3 所示。
构造器
Bean 类还有一个默认的公有构造器,由于程序员没有定义显式的构造器,所以它是由编译 器生成的。这个默认的公有构造器被生成为 Bean() { super(); }。这个构造器的字节代码 如下:
ALOAD 0
INVOKESPECIAL java/lang/Object <init> ()V
RETURN
第一条指令将 this 压入操作数栈中。第二条指令从栈中弹出这个值,并调用在 Object 对 象中定义的 <init> 方法。这对应于 super() 调用,也就是对超类 Object 构造器的调用。 在这里可以看到,在已编译类和源类中对构造器的命名是不同的:在编译类中,它们总是被命 名 为 <init>,而在源类中,它们的名字与定义它们的类同名。最后一条指令返回调用者。
更复杂的一个方法
public void checkAndSetF(int f){
if (f >= 0) {
this.f =f;
} else {
throw new IllegalArgumentException();
}
}
这个新 setter 方法的字节代码如下:
ILOAD 1
IFLT label
ALOAD 0
ILOAD 1
PUTFIELD pkg/Bean f I
GOTO end
label:
NEW java/lang/IllegalArgumentException
DUP
INVOKESPECIAL java/lang/IllegalArgumentException<init> ()V
ATHROW
end:
RETURN
第一条指令将初始化为 f 的局部变量 1 压入操作数栈。IFLT 指令从栈中弹出这个值,并将它与 0 进行比较。如果它小于**(LT)0**,则跳转到由 label 标记指定的指令,否则不做任何事 情,继续执行下一条指令。接下来的三条指令与 setF 方法中相同。GOTO 指令无条件跳转到由 end 标记指定的指令,也就是 RETURN 指令。label 和 end 标记之间的指令创建和抛出一个 异 常:NEW 指令创建一个异常对象,并将它压入操作数栈中。DUP 指令在栈中重复这个值。 INVOKESPECIAL 指令弹出这两个副本之一,并对其调用异常构造器。最后,ATHROW 指令弹出 剩下的副本,并将它作为异常抛出(所以不会继续执行下一条指令)。
3.1.4. 异常处理器
不存在用于捕获异常的字节代码:而是将一个方法的字节代码与一个异常处理器列表关联在一起,这个列表规定了在某方法中一给定部分抛出异常时必须执行的代码。异常处理器类似于 try catch 块:它有一个范围,也就是与 try 代码块内容相对应的一个指令序列,还有一个处 理器,对应于 catch 块中的内容。这个范围由一个起始标记和一个终止标记指定,处理器由一 个起始标记指定。比如下面的源代码:
public static void sleep(long d) {
try {
Thread.sleep(d);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
可被编译为
TRYCATCHBLOCK try catch catch java/lang/InterruptedException
try:
LLOAD 0
INVOKESTATIC java/lang/Thread sleep (J)V
RETURN
catch:
INVOKEVIRTUAL java/lang/InterruptedException printStackTrace ()V
RETURN
Try 和 catch 标记之间的代码对应于 try 块,而 catch 标记之后的代码对应于 catch。 TRYCATCHBLOCK 行指定了一个异常处理器,覆盖了 try 和 catch 标记之间的范围,有一个开 始 于 catch 标记的处理器,用于处理一些异常,这些异常的类是 InterruptedException 的子类。这意味着,如果在 try 和 catch 之间抛出了这样一个异常,栈将被清空,异常被 压入 这个空栈中,执行过程在 catch 处继续。
3.1.5. 帧 frame
除了字节代码指令之外,用 Java 6 或更高版本编译的类中还包含一组栈映射帧,用于加快 Java 虚拟机中类验证过程的速度
。栈映射帧给出一个方法的执行帧在执行过程中某一时刻的状 态。更准确地说,它给出了在就要执行某一特定字节代码指令之前,每个局部变量槽和每个操作 数栈槽中包含的值的类型。
例如,如果考虑上一节的 getF 方法,可以定义三个栈映射帧,给出执行帧在即将执行ALOAD、即将执行 GETFIELD 和即将执行 IRETURN 之前的状态。这三个栈映射帧对应于图 3.2 给出的三种情况,可描述如下,其中第一个方括号中的类型对应于局部变量,其他类型对应于操 作数栈:
如下代码之前的执行帧状态 | 指令 |
---|---|
[pkg/Bean] [] | ALOAD 0 |
[pkg/Bean] [pkg/Bean] | GETFIELD |
[pkg/Bean] [I] | IRETURN |
可以对 checkAndSetF 方法进行相同操作:
如下代码之前的执行帧状态 | |
---|---|
[pkg/Bean I] [] | ILOAD 1 |
[pkg/Bean I] [I] | IFLT label |
[pkg/Bean I] [] | ALOAD 0 |
[pkg/Bean I] [pkg/Bean] | ILOAD 1 |
[pkg/Bean I] [pkg/Bean I] [pkg/Bean I] [] | PUTFIELD |
[pkg/Bean I] [] | GOTO end |
[pkg/Bean I] [] | label : |
[pkg/Bean I] [Uninitialized(label)] | NEW |
[pkg/Bean I] [Uninitialized(label) Uninitialized(label)] [pkg/Bean I] | INVOKESPECIAL |
[pkg/Bean I] [] | end : |
[pkg/Bean I] [] | RETURN |
除了 Uninitialized(label) 类型之外,它与前面的方法均类似。这是一种仅在栈映射帧 中使用的特殊类型,它指定了一个对象,已经为其分配了内存,但还没有调用其构造器。参数规 定了创建此对象的指令。对于这个类型的值,只能调用一种方法,那就是构造器。在调用它时, 在 帧中出现的所有这一类型都被代以一个实际类型,这里是 IllegalArgumentException。 栈映 射帧可使用三种其他特殊类型:UNINITIALIZED_THIS 是构造器中局部变量 0 的初始类 型, TOP 对应于一个未定义的值,而 NULL 对应于 null。
上文曾经说过,从 Java 6 开始,除了字节代码之外,已编译类中还包含了一组栈映射帧。为 节省空间,已编译方法中并没有为每条指令包含一个帧:事实上,它仅为那些对应于跳转目标或 异常处理器的指令,或者跟在无条件跳转指令之后的指令包含帧。事实上,可以轻松、快速地由 这些帧推断出其他帧。
在 checkAndSetF 方法的情景中,这意味着仅存储两个帧:一个用于 NEW 指令,因为 它是 IFLT 指令的目标,还因为它跟在无条件跳转 GOTO 指令之后,另一个用于 RETURN 指令,因为 它是 GOTO 指令的目标,还因为它跟在“无条件跳转”ATHROW 指令之后。
为节省更多空间,对每一帧都进行压缩:仅存储它与前一帧的差别,而初始帧根本不用存 储, 可以轻松地由方法参数类型推导得出。在 checkAndSetF 方法中,必须存储的两帧是相 同的, 都等于初始帧,所以它们被存储为单字节值,由 F_SAME 助记符表示。可以在与这些 帧相关联 的字节代码指令之前给出这些帧。这就给出了 F_SAME 方法的最终字节代码:
ILOAD 1
IFLT label
ALOAD 0
ILOAD 1
PUTFIELD pkg/Bean f I GOTO end
label:
F_SAME
NEW java/lang/IllegalArgumentException
DUP
INVOKESPECIAL java/lang/IllegalArgumentException <init> ()V
ATHROW
end:
F_SAME
RETURN
3.2. 接口和组件
3.2.1. 介绍
用于生成和转换已编译方法的 ASM API 是基于 MethodVisitor 抽象类的(见图 3.4),它 由 ClassVisitor 的 visitMethod 方法返回。除了一些与注释和调试信息有关的方法之外(这些方法在下一章解释),这个类为每个字节代码指令类别定义了一个方法,其依据就是这些指令 的 参数个数和类型(这些类别并非对应于 3.1.2 节给出的类别)。这些方法必须按以下顺序调用(在 MethodVisitor 接口的 Javadoc 中还规定了其他一些约束条件):
visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn | visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd
这就意味着,对于非抽象方法,如果存在注释和属性的话,必须首先访问它们,然后是该方 法的字节代码。对于这些方法,其代码必须按顺序访问,位于对 visitCode 的调用(有且仅有 一个调用)与对 visitMaxs 的调用(有且仅有一个调用)之间。
abstract class MethodVisitor {
// public accessors ommited MethodVisitor(int api);
MethodVisitor(int api, MethodVisitor mv);
AnnotationVisitor visitAnnotationDefault();
AnnotationVisitor visitAnnotation(String desc, boolean visible);
AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible);
void visitAttribute(Attribute attr);
void visitCode();
void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack);
void visitInsn(int opcode);
void visitIntInsn(int</