程序分析、程序生成和程序转换都是非常有用的技术,可在许多应用环境下使用:
- 程序分析,既可能只是简单的语法分析(syntaxic parsing),也可能是完整的语义分析 (sematic analysis),可用于查找应用程序中的潜在 bug、检测未被用到的代码、对代码实施逆向工程,等等。
- 程序生成,在编译器中使用。这些编译器不仅包括传统编译器,还包括用于分布式程序设计的 stub 编译器或 skeleton 编译器,以及 JIT(即时)编译器,等等。
- 程序转换可,用于优化或混淆(obfuscate)程序、向应用程序中插入调试或性能监视代码,用于面向方面的程序设计,等等。
所有这些技术都可针对任意程序设计语言使用,但对于不同语言,其使用的难易程度可能会有所不同。对于 Java 语言,它们可用于 Java 源代码或编译后的 Java 类。在使用经过编译的类时, 其好处之一显然就是不需要源代码。因此,程序转换可用于任何应用程序,既包括保密的源代码, 也包含商业应用程序。使用已编译类的另一个好处是,有可能在运行时,在马上就要将类加载到 Java 虚拟机之前,对类进行分析、生成或转换(在运行时生成和编译源代码也可以,但其速度很慢,而且需要一个完整的 Java 编译器)。其好处是,诸如 stub 编译器或方面编织器等工具对用户变为透明。
由于程序分析、生成和转换技术的用途众多,所以人们针对许多语言实现了许多用于分析、生成和转换程序的工具,这些语言中就包括 Java 在内。ASM 就是为 Java 语言设计的工具之一, 用于进行运行时(也是脱机的)类生成与转换。于是,人们设计了 ASM①库,用于处理经过编译的 Java 类。这个库的设计使其尽可能保持快速和小型化。对于那些在运行时使用 ASM 进行动态类生成或转换的应用程序来说,尽可能提高库的运行速度是非常重要的,这样可以保证这些应用程序的速度不致下降过多。而保持 ASM 库的小型化也非常重要,一方面是为了在内存有限的环境中使用,另一方面,也为了避免使那些使用 ASM 的小型应用程序或库增大过多。 ASM 并不是惟一可生成和转换已编译 Java 类的工具,但它是最新、最高效的工具之一,可从 http://asm.objectweb.org 下载。其主要优点如下:
① ASM 的名字没有任何含义:它只是引用 C 语言中的 语言编写的函数。asm 关键字,这个关键字允许执行一些用汇编
- 有一个简单的模块 API,设计完善、使用方便。
- 文档齐全,拥有一个相关的 Eclipse 插件。
- 支持最新的 Java 版本——Java 7。
- 小而快、非常可靠。
- 拥有庞大的用户社区,可以为新用户提供支持。
- 源许可开放,几乎允许任意使用。
ASM 库的目的是生成、转换和分析以字节数组表示的已编译 Java 类(它们在磁盘中的存储和在 Java 虚拟机中的加载都采用这种字节数组形式)。为此,ASM 提供了一些工具,使用高于字节级别的概念来读写和转换这种字节数组,这些概念包括数值常数、字符串、Java 标识符、Java 类型、Java 类结构元素,等等。注意,ASM 库的范围严格限制于类的读、写、转换和分析。具体来说,类的加载过程就超出了它的范围之外。
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 都是仅能同时维护一个类,而且独立于其他类,也就是说,它们不会维护有关类层级结构的信息,如果类的转换影响到其他类,那其他这些类的修改应当由用户负责完成。
注意,这两个 API 都是仅能同时维护一个类,而且独立于其他类,也就是说,它们不会维护有关类层级结构的信息,如果类的转换影响到其他类,那其他这些类的修改应当由用户负责完成。
ASM 应用程序拥有一个很强壮的体系结构方面(aspect)。事实上,对于基于事件的 API,其组织结构是围绕事件生成器(类分析器)、事件使用器(类写入器)和各种预定义的事件筛选器进行的,在这一结构中可以添加用户定义的生成器、使用器和筛选器。因此,这一 API 的使用分为两个步骤:
- 将事件生成器、筛选器和使用器组件组装为可能很复杂的体系结构,
- 然后启动事件生成器,以执行生成或转换过程。
基于对象的 API 也有一个体系结构方面:实际上,用于操作类树的类生成器或转换器组件是可以组成形成的,它们之间的链接代表着转换的顺序。
尽管典型 ASM 应用程序中的大多数组件体系结构都非常简单,但还是可以想象一下类似于如下所示的复杂体系结构,其中的箭头表示在类分析器、写入器或转换器之间进行的基于事件或基于对象的通信,在整个链中的任何位置,都可能会在基于事件与基于对象的表示之间进行转换:
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。
印刷约定
- 斜体用于强调句子中的元素。(译者注:中文中一般改为加粗显示)
- 定宽字体用于表示代码段。
- 粗体定宽字体用于强调代码元素。
- 斜体定宽字体用于表示标记和代码中的变量部分。
感谢 François Horn 在制作本文档期间提供的宝贵评论,这些意见极大地提升了本文档的结构和可读性。