Apache Byte Code Engineering Library (BCEL)可以深入 Java 类的字节码。可以用它 转换现有的类表示或者构建新的类,因为 BCEL 在单独的 JVM 指令级别上进行操作,所以可 以让您对代码有最强大的控制。不过,这种能力的代价是复杂性。在本文中,Java 顾问 Dennis Sosnoski 介绍了 BCEL 的基本内容,并引导读者完成一个示例 BCEL 应用程序,这 样您就可以自己决定是否值得以这种复杂性来换取这种能力。
在本系列的最后三篇文章中,我展示了如何用 Javassist 框架操作类。这次我将用一种很 不同的方法操纵字节码——使用 Apache Byte Code Engineering Library (BCEL)。与 Javassist 所支持的源代码接口不同,BCEL 在实际的 JVM 指令层次上进行操作。在希望对 程序执行的每一步进行控制时,底层方法使 BCEL 很有用,但是当两者都可以胜任时,它也 使 BCEL 的使用比 Javassist 要复杂得多。
我将首先讨论 BCEL 基本体系结构,然后本文的大部分内容将讨论用 BCEL 重新构建我的 第一个 Javassist 类操作的例子。最后简要介绍 BCEL 包中提供的一些工具和开发人员用 BCEL 构建的一些应用程序。
BCEL 类访问
BCEL 使您能够同样具备 Javassist 提供的分析、编辑和创建 Java 二进制类的所有基本 能力。BCEL 的一个明显区别是每项内容都设计为在 JVM 汇编语言的级别、而不是 Javassist 所提供的源代码接口上工作。除了表面上的差别,还有一些更深层的区别,包括 在 BCEL 中组件的两个不同层次结构的使用——一个用于检查现有的代码,另一个用于创建 新代码。我假定读者已经通过本系列前面的文章熟悉了 Javassist(请参阅侧栏 不要错过本 系列的其余部分)。因此我将主要介绍在开始使用 BCEL 时,可能会让您感到迷惑的那些不 同之处。
与 Javassist 一样, BCEL 在类分析方面的功能基本上与 Java 平台通过 Relfection API 直接提供的功能是重复的。这种重复对于类操作工具箱来说是必要的,因为一般不希望 在所要操作的类被修改 之前就装载它们。
BCEL 在 org.apache.bcel 包中提供了一些基本常量定义,但是除了这些定义,所有分析 相关的代码都在 org.apache.bcel.classfile 包中。这个包中的起点是 JavaClass 类。这 个类在用 BCEL 访问类信息时起的作用与使用常规 Java 反射时, java.lang.Class 的作用 一样。 JavaClass 定义了得到这个类的字段和方法信息,以及关于父类和接口的结构信息的 方法。 与 java.lang.Class 不同,JavaClass 还提供了对类的内部信息的访问,包括常量 池和属性,以及作为字节流的完整二进制类表示。
JavaClass 实例通常是通过解析实际的二进制类创建的。BCEL 提供了 org.apache.bcel.Repository 类用于处理解析。在默认情况下,BCEL 解析并缓冲在 JVM 类 路径中找到的类表示,从 org.apache.bcel.util.Repository 实例中得到实际的二进制类表 示(注意包名的不同)。 org.apache.bcel.util.Repository 实际上是二进制类表示的源代 码的接口。在默认源代码中使用类路径的地方,可以用查询类文件的其他路径或者其他访问 类信息的方法替换。
改变类
除了对类组件的反射形式的访问, org.apache.bcel.classfile.JavaClass 还提供了改 变类的方法。可以用这些方法将任何组件设置为新值。不过一般不直接使用它们,因为包中 的其他类不以任何合理的方式支持构建新版本的组件。相反,在 org.apache.bcel.generic 包中有完全单独的一组类,它提供了 org.apache.bcel.classfile 类所表示的同一组件的可 编辑版本。
就 像 org.apache.bcel.classfile.JavaClass 是使用 BCEL 分析现有类的起点一样, org.apache.bcel.generic.ClassGen 是创建新类的起点。它还用于修改现有的类——为了处 理这种情况,有一个以 JavaClass 实例为参数的构造函数,并用它初始化 ClassGen 类信息 。修改了类以后,可以通过调用一个返回 JavaClass 的方法从 ClassGen 实例得到可使用的 类表示,它又可以转换为一个二进制类表示。
听起来有些乱?我想是的。事实上,在两个包之间来回转是使用 BCEL 的一个最主要的缺 点。重复的类结构总有些碍手碍脚,所以如果频繁使用 BCEL,那么可能需要编写一个包装器 类,它可以隐藏其中一些不同之处。在本文中,我将主要使用 org.apache.bcel.generic 包 类,并避免使用包装器。不过在您自己进行开发时要记住这一点。
除了 ClassGen , org.apache.bcel.generic 包还定义了管理不同类组件的结构的类。 这些结构类包括用于处理常量池的 ConstantPoolGen 、用于字段和方法的 FieldGen 和 MethodGen 和处理一系列 JVM 指令的 InstructionList 。最后, org.apache.bcel.generic 包还定义了表示每一种类型的 JVM 指令的类。可以直接创建这些 类的实例,或者在某些情况下使用 org.apache.bcel.generic.InstructionFactory helper 类。使用 InstructionFactory 的好处是它处理了许多指令构建的簿记细节(包括根据指令 的需要在常量池中添加项)。在下面一节您将会看到如何使所有这些类协同工作。
用 BCEL 进行类操作
作为使用 BCEl 的一个例子,我将使用 第 4 部分中的一个 Javassist 例子——测量执 行一个方法的时间。我甚至采用了与使用 Javassist 时的相同方式:用一个改过的名字创建 要计时的原方法的一个副本,然后,通过调用改名后的方法,利用包装了时间计算的代码来 替换原方法的主体。