在经过一段时间的休息之后,Dennis Sosnoski 又回来推出了他的 Java 编程的动态性系 列的第 5 部分。您已在前面的文章中看到了如何编写用于转换 Java 类文件以改变代码行为的程序。在本期中,Dennis将展示如何使用 Javassist 框架,把转换与实际的类加载过程结合起来,用以进行灵活的“即时”面向方面的特性处理。这种方法允许您决定想要在运行时改变的内容,并潜地在每次运行程序 时做出不同的修改。在整个过程中,您还将更深入地了解向JVM 中加载类的一般问题。
在第 4 部分“ 用 Javassist 进行类转换”中,您学习了如何使用 Javassist 框架来转换编译器生成的 Java 类文件,同时写回修改过的类文件。这种类文件转换步骤对于做出持久变更是很理想的,但是如果想要在每次执行应用程序时做出不同的变更,这种方法就不一定很方便。对于这种暂时的变更,采用在您实际启动应用程序时起作用的方法要好得多。
JVM 体系结构为我们提供了这样做的便利途径――通过使用 classloader 实现。通过使用 classloader 挂钩(hook),您可以拦截将类加载到 JVM 中的过程,并在实际加载这些类之前转换它们。为了说明这个过程是如何工作的,我将首先展示类加载过程的直接拦截,然后展示 Javassist 如何提供了一种可在您的应用程序中使用的便利捷径。在整个过程中,我将利用取自本系列以前文章中的代码片断。
|
运行 Java 应用程序的通常方式是作为参数向 JVM 指定主类。这对于标准操作没有什么问题,但是它没有提供及时拦截类加载过程的任何途径,而这种拦截对大多数程序来说是很有用的。正如我在第 1 部分“ 类和类装入”中所讨论的,许多类甚至在主类还没有开始执行之前就已经加载了。要拦截这些类的加载,您需要在程序的执行过程中进行某种程度的重定向。
幸运的是,模拟 JVM 在运行应用程序的主类时所做的工作是相当容易的。您所需做的就是使用反射(这是在不得 第 2 部分 中介绍的)来首先找到指定类中的静态 main()
方法,然后使用预期的命令行参数来调用它。清单 1 提供了完成这个任务的示例代码(为简单起见,我省略了导入和异常处理语句):
|
要使用这个类来运行 Java 应用程序,只需将它指定为 java
命令的目标类,后面跟着应用程序的主类和想要传递给应用程序的其他任何参数。换句话说,如果用于运行 Java 应用程序的命令为:
|
您相应地要通过如下命令使用 Run
类来运行应用程序:
|
就其本身而言,清单 1 中短小的 Run
类不是非常有用。为了实现拦截类加载过程的目标,我们需要采取进一步的动作,对应用程序类定义和使用我们自己的 classloader。
正 如我们在第 1 部分中讨论的,classloader 使用一个树状层次结构。每个 classloader(JVM 用于核心 Java 类的根 classloader 除外)都具有一个父 classloader。Classloader 应该在独自加载类之前检查它们的父 classloader,以防止当某个层次结构中的多个 classloader 加载同一个类时可能引发的冲突。首先检查父 classloader 的过程称为 委托――classloader 将加载类的责任委托给最接近根的 classloader,后者能够访问要加载类的信息。
当 清单 1 中的 Run
程序开始执行时,它已经被 JVM 默认的 System classloader(您定义的 classpath 所指定的那一个)加载了。为了符合类加载的委托规则,我们需要对相同的父 classloader 使用完全相同的 classpath 信息和委托,从而使我们的 classloader 成为 System classloader 的真正替代者。幸运的是,JVM 当前用于 System classloader 实现的 java.net.URLClassLoader
类提供了一种检索 classpath 信息的容易途径,它使用了 getURLs()
方法。为了编写 classloader,我们只需从 java.net.URLClassLoader
派生子类,并初始化基类以使用相同的 classpath 和父 classloader 作为加载主类的 System classloader。清单 2 提供了这种方法的具体实现:
|
我们已从 java.net.URLClassLoader
派生了我们自己的 VerboseLoader
类,它列出正在被加载的所有类,同时指出哪些类是由这个 loader 实例(而不是委托父 classloader)加载的。这里同样为简洁起见而省略了导入和异常处理语句。
VerboseLoader
类中的前两个方法 loadClass()
和 findClass()
重载了标准的 classloader 方法。 loadClass()
方法分别针对 classloader 请求的每个类作了调用。在此例中,我们仅让它向控制台打印一条消息,然后调用它的基类版本来执行实际的处理。基类方法实现了标准 classloader 委托行为,即首先检查父 classloader 是否能够加载所请求的类,并且仅在父 classloader 无法加载该类时,才尝试使用受保护的 findClass()
方法来直接加载该类。对于 findClass()
的 VerboseLoader
实现,我们首先调用重载的基类实现,然后在调用成功(在没有抛出异常的情况下返回)时打印一条消息。
VerboseLoader
的 main()
方法或者从用于包含类的 loader 中获得 classpath URL 的列表,或者在与不属于 URLClassLoader
的实例的 loader 一起使用的情况下,简单地使用当前目录作为唯一的 classpath 条目。不管采用哪种方式,它都会列出实际正在使用的路径,然后创建 VerboseLoader
类的一个实例,并使用该实例来加载命令行上指定的目标类。该逻辑的其余部分(即查找和调用目标类的 main()
方法)与 清单 1 中的 Run
代码相同。
清单 3 显示了 VerboseLoader
命令行和输出的一个例子,它用于调用清单 1 中的 Run
应用程序:
|
在此例中,唯一直接由 VerboseLoader
加载的类是 Run
类。 Run
使用的其他所有类都是核心 Java 类,它们是通过父 classloader 使用委托来加载的。这其中的大多数(如果不是全部的话)核心类实际上都会在 VerboseLoader
应用程序本身的启动期间加载,因此父 classloader 将只返回一个指向先前创建的 java.lang.Class
实例的引用。
清单 2 中的 VerboseClassloader
展示了拦截类加载的基本过程。为了在加载时修改类,我们可以更进一步,向 findClass()
方法添加代码,把二进制类文件当作资源来访问,然后使用该二进制数据。Javassist 实际上包括了直接完成此类拦截的代码,因此与其进一步扩充这个例子,我们不如看看如何使用 Javassist 实现。
使用 Javassist 来拦截类加载的过程要依赖我们在 第 4 部分 中使用的相同 javassist.ClassPool
类。在该文中,我们通过名称直接从 ClassPool
请求类,以 javassist.CtClass
实例的形式取回该类的 Javassist 表示。然而,那并不是使用 ClassPool
的唯一方式――Javassist 还以 javassist.Loader
类的形式,提供一个使用 ClassPool
作为其类数据源的 classloader。
为了允许您在加载类时操作它们, ClassPool
使用了一个 Observer 模式。您可以向 ClassPool
的构造函数传递预期的观察者接口(observer interface)的一个实例 javassist.Translator
。每当从 ClassPool
请求一个新的类,它都调用观察者的 onWrite()
方法,这个方法能够在 ClassPool
交付类之前修改该类的表示。
javassist.Loader
类包括一个便利的 run()
方法,它加载目标类,并且使用所提供的参数数组来调用该类的 main()
方法(就像在 清单 1 中一样)。清单 4 展示了如何使用 Javassist 类和这个方法来加载和运行目标应用程序类。这个例子中简单的 javassist.Translator
观察者实现仅只是打印一条关于正在被请求的类的消息。
|
下面是 JavassistRun
命令行和输出的一个例子,其中使用它来调用 清单 1 中的 Run
应用程序。
|
|
我们在 第 4 部分中 分析过的方法定时修改对于隔离性能问题来说可能一个很有用的工具,但它的确需要一个更灵活的接口。在该文中,我们只是将类和方法名称作为参数传递给程序, 程序加载二进制类文件,添加定时代码,然后写回该类。对于本文,我们将把代码转换为使用加载时修改方法,并将它转换为可支持模式匹配,用以指定要定时的类 和方法。
在加载类时更改代码以处理这种修改是很容易的。在清单 4 中的 javassist.Translator
代码的基础上,当正在写出的类名称与目标类名称匹配时,我们可以仅从 onWrite()
调用用于添加定时信息的方法。清单 5 展示了这一点(没有包含 addTiming()
的全部细节――请参阅第 4 部分以了解这些细节)。
|
如清单 5 所示,除了使方法定时代码在加载时工作外,在指定要定时的方法时增加灵活性也是很理想的。我最初使用 Java 1.4 java.util.regex
包中的正则表达式匹配支持来实现这点,然后意识到它并没有真正带来我想要的那种灵活性。问题在于,用于选择要修改的类和方法的有意义的模式种类无法很好地适应正则表达式模型。
那么哪种模式对于选择类和方法 有意 义呢?我想要的是在模式中使用类和方法的任何几个特征的能力,包括实际的类和方法名称、返回类型,以及调用参数类型。另一方面,我不需要对名称和类型进行 真正灵活的比较――简单的相等比较就能处理我感兴趣的大多数情况,而对该比较添加基本的通配符就能处理其余的所有情况了。处理这种情况的最容易方法是使模 式看起来像标准的 Java 方法声明,另外再进行一些扩展。
关于这种方法的例子,下面是几个与 test.StringBuilder
类的 String buildString(int)
方法相匹配的模式:
|
这些模式的通用模式首先是一个可 选的返回类型(具有精确的文本),然后是组合起来的类和方法名称模式(具有“*”通配字符),最后是参数类型列表(具有精确的文本)。如果提供了返回类 型,必须使用一个空格将它与方法名称匹配相隔离,而参数列表则跟在方法名称匹配后面。为了使参数匹配更灵活,我通过两种方式来设置它。如果所给的参数是圆 括号括起的列表,它们必须精确匹配方法参数。如果它们是使用方括号(“[]”)来括起的,所列出的类型全都必须 作为匹配方法的参数来提供,不过该方法可以按任何顺序使用它们,并且还可以使用附加的参数。因此 *buildString(int, java.lang.String)
将匹配其名称以“buildString”结尾的任何方法,并且这些方法精确地按顺序接受一个 int
类型和一个 String
类型的参数。 *buildString[int,java.lang.String]
将匹配具有相同名称的方法,但是这些方法接受两个 或更多的 参数,其中一个是 int
类型,另一个是 java.lang.String
类型。
清单 6 给出了我编写来处理这些模式的 javassist.Translator
子类的简略版本。实际的匹配代码与本文并不真正相关,不过如果您想要查看它或亲自使用它,我已将它包括在了下载文件中(请参阅 参考资料)。使用这个 TimingTranslator
的主程序类是 BatchTiming
,它也包括在下载文件中。
|
|
在上两篇文章中,您已经看到了如何使用 Javassist 来处理基本的转换。对于下一篇文章,我们将探讨这个框架的高级特性,这些特性提供用于编辑字节代码的查找和替换技术。这些特性使得对程序行为做出系统性的 变更很容易,其中包括诸如拦截所有方法调用或所有字段访问这样的变更。它们是理解为什么 Javassist 是 Java 程序中提供面向方面支持的卓越框架的关键。请下个月再回来看看如何能够使用 Javassist 来揭示应用程序中的方面(aspect)。