首页 / 软件开发 / JAVA / Java编程的动态性, 第4部分: 用Javassist进行类转换
Java编程的动态性, 第4部分: 用Javassist进行类转换2011-04-09 IBM Dennis M. Sosnoski厌倦了只能按编写好源代码的方式执行的 Java 类了吗?那么打起精神吧,因为您就要发 现如何将编译器编译好的类进行改造的方法了!在本文中,Java 顾问 Dennis Sosnoski 通 过介绍字节码操作库 Javassist 将他的 Java 编程的动态性系列带入高潮,Javassist 是广 泛使用的 JBoss 应用服务器中加入的面向方面的编程功能的基础。您会看到到用 Javassist 转换现有类的基本内容,并且了解到这种用框架源代码处理类的方法的威力和局限性。讲过了 Java 类格式和利用反射进行的运行时访问后,本系列到了进入更高级主题的时候 了。本月我将开始本系列的第二部分,在这里 Java 类信息只不过是由应用程序操纵的另一 种形式的数据结构而已。我将这个主题的整个内容称为 classworking。我将以 Javassist 字节码操作库作为对 classworking 的讨论的开始。Javassist 不仅 是一个处理字节码的库,而且更因为它的另一项功能使得它成为试验 classworking 的很好 的起点。这一项功能就是:可以用 Javassist 改变 Java 类的字节码,而无需真正了解关于 字节码或者 Java 虚拟机(Java virtual machine JVM)结构的任何内容。从某方面将这一功 能有好处也有坏处 -- 我一般不提倡随便使用不了解的技术 -- 但是比起在单条指令水平上 工作的框架,它确实使字节码操作更可具有可行性了。Javassist 基础Javassist 使您可以检查、编辑以及创建 Java 二进制类。检查方面基本上与通过 Reflection API 直接在 Java 中进行的一样,但是当想要修改类而不只是执行它们时,则另 一种访问这些信息的方法就很有用了。这是因为 JVM 设计上并没有提供在类装载到 JVM 中 后访问原始类数据的任何方法,这项工作需要在 JVM 之外完成。Javassist 使用 javassist.ClassPool 类跟踪和控制所操作的类。这个类的工作方式与 JVM 类装载器非常相似,但是有一个重要的区别是它不是将装载的、要执行的类作为应用程 序的一部分链接,类池使所装载的类可以通过 Javassist API 作为数据使用。可以使用默认 的类池,它是从 JVM 搜索路径中装载的,也可以定义一个搜索您自己的路径列表的类池。甚 至可以直接从字节数组或者流中装载二进制类,以及从头开始创建新类。装载到类池中的类由 javassist.CtClass 实例表示。与标准的 Java java.lang.Class 类一样, CtClass 提供了检查类数据(如字段和方法)的方法。不过,这只是 CtClass 的 部分内容,它还定义了在类中添加新字段、方法和构造函数、以及改变类、父类和接口的方 法。奇怪的是,Javassist 没有提供删除一个类中字段、方法或者构造函数的任何方法。字段、方法和构造函数分别由 javassist.CtField、javassist.CtMethod 和 javassist.CtConstructor 的实例表示。这些类定义了修改由它们所表示的对象的所有方法 的方法,包括方法或者构造函数中的实际字节码内容。所有字节码的源代码Javassist 让您可以完全替换一个方法或者构造函数的字节码正 文,或者在现有正文的开始或者结束位置选择性地添加字节码(以及在构造函数中添加其他一 些变量)。不管是哪种情况,新的字节码都作为类 Java 的源代码声明或者 String 中的块传 递。Javassist 方法将您提供的源代码高效地编译为 Java 字节码,然后将它们插入到目标 方法或者构造函数的正文中。Javassist 接受的源代码与 Java 语言的并不完全一致,不过主要的区别只是增加了一些 特殊的标识符,用于表示方法或者构造函数参数、方法返回值和其他在插入的代码中可能用 到的内容。这些特殊标识符以符号 $ 开头,所以它们不会干扰代码中的其他内容。对于在传递给 Javassist 的源代码中可以做的事情有一些限制。第一项限制是使用的格 式,它必须是单条语句或者块。在大多数情况下这算不上是限制,因为可以将所需要的任何 语句序列放到块中。下面是一个使用特殊 Javassist 标识符表示方法中前两个参数的例子, 这个例子用来展示其使用方法:{
System.out.println("Argument 1: " + $1);
System.out.println("Argument 2: " + $2);
}对于源代码的一项更实质性的限制是不能引用在所添加的声明或者块外声明的局部变量。 这意味着如果在方法开始和结尾处都添加了代码,那么一般不能将在开始处添加的代码中的 信息传递给在结尾处添加的代码。有可能绕过这项限制,但是绕过是很复杂的 -- 通常需要 设法将分别插入的代码合并为一个块。用 Javassist 进行 Classworking作为使用 Javassist 的一个例子,我将使用一个通常直接在源代码中处理的任务:测量 执行一个方法所花费的时间。这在源代码中可以容易地完成,只要在方法开始时记录当前时 间、之后在方法结束时再次检查当前时间并计算两个值的差。如果没有源代码,那么得到这 种计时信息就要困难得多。这就是 classworking 方便的地方 -- 它让您对任何方法都可以 作这种改变,并且不需要有源代码。