`

Java 编程的动态性,第 5 部分: 动态转换类

阅读更多
在经过一段时间的休息之后,Dennis Sosnoski 又回来推出了他的 Java 编程的动态性系 列的第 5 部分。您已在前面的文章中看到了如何编写用于转换 Java 类文件以改变代码行为的程序。在本期中,Dennis将展示如何使用 Javassist 框架,把转换与实际的类加载过程结合起来,用以进行灵活的“即时”面向方面的特性处理。这种方法允许您决定想要在运行时改变的内容,并潜地在每次运行程序 时做出不同的修改。在整个过程中,您还将更深入地了解向JVM 中加载类的一般问题。

在第 4 部分“ 用 Javassist 进行类转换”中,您学习了如何使用 Javassist 框架来转换编译器生成的 Java 类文件,同时写回修改过的类文件。这种类文件转换步骤对于做出持久变更是很理想的,但是如果想要在每次执行应用程序时做出不同的变更,这种方法就不一定很方便。对于这种暂时的变更,采用在您实际启动应用程序时起作用的方法要好得多。

JVM 体系结构为我们提供了这样做的便利途径――通过使用 classloader 实现。通过使用 classloader 挂钩(hook),您可以拦截将类加载到 JVM 中的过程,并在实际加载这些类之前转换它们。为了说明这个过程是如何工作的,我将首先展示类加载过程的直接拦截,然后展示 Javassist 如何提供了一种可在您的应用程序中使用的便利捷径。在整个过程中,我将利用取自本系列以前文章中的代码片断。

不要错过本系列的其余文章

第 1 部分,“ 类和类装入”(2003 年 4 月)

第 2 部分,“ 引入反射”(2003 年 6 月)

第 3 部分,“ 应用反射”(2003 年 7 月)

第 4 部分,“ 用 Javassist 进行类转换”(2003 年 9 月)

加载区域

运行 Java 应用程序的通常方式是作为参数向 JVM 指定主类。这对于标准操作没有什么问题,但是它没有提供及时拦截类加载过程的任何途径,而这种拦截对大多数程序来说是很有用的。正如我在第 1 部分“ 类和类装入”中所讨论的,许多类甚至在主类还没有开始执行之前就已经加载了。要拦截这些类的加载,您需要在程序的执行过程中进行某种程度的重定向。

幸运的是,模拟 JVM 在运行应用程序的主类时所做的工作是相当容易的。您所需做的就是使用反射(这是在不得 第 2 部分 中介绍的)来首先找到指定类中的静态 main() 方法,然后使用预期的命令行参数来调用它。清单 1 提供了完成这个任务的示例代码(为简单起见,我省略了导入和异常处理语句):


清单 1. Java 应用程序运行器


public class Run
{
public static void main(String[] args) {
if (args.length >= 1) {
try {

// load the target class to be run
Class clas = Run.class.getClassLoader().
loadClass(args[0]);

// invoke "main" method of target class
Class[] ptypes =
new Class[] { args.getClass() };
Method main =
clas.getDeclaredMethod("main", ptypes);
String[] pargs = new String[args.length-1];
System.arraycopy(args, 1, pargs, 0, pargs.length);
main.invoke(null, new Object[] { pargs });

} catch ...
}

} else {
System.out.println
("Usage: Run main-class args...");
}
}
}

要使用这个类来运行 Java 应用程序,只需将它指定为 java 命令的目标类,后面跟着应用程序的主类和想要传递给应用程序的其他任何参数。换句话说,如果用于运行 Java 应用程序的命令为:


java test.Test arg1 arg2 arg3

您相应地要通过如下命令使用 Run 类来运行应用程序:


java Run test.Test arg1 arg2 arg3

拦截类加载

就其本身而言,清单 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 提供了这种方法的具体实现:


清单 2. 一个详细的 classloader


public class VerboseLoader extends URLClassLoader
{
protected VerboseLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}

public Class loadClass(String name)
throws ClassNotFoundException {
System.out.println("loadClass: " + name);
return super.loadClass(name);
}

protected Class findClass(String name)
throws ClassNotFoundException {
Class clas = super.findClass(name);
System.out.println("findclass: loaded " + name +
" from this loader");
return clas;
}

public static void main(String[] args) {
if (args.length >= 1) {
try {

// get paths to be used for loading
ClassLoader base =
ClassLoader.getSystemClassLoader();
URL[] urls;
if (base instanceof URLClassLoader) {
urls = ((URLClassLoader)base).getURLs();
} else {
urls = new URL[]
{ new File(".").toURI().toURL() };
}

// list the paths actually being used
System.out.println("Loading from paths:");
for (int i = 0; i < urls.length; i++) {
System.out.println(" " + urls[i]);
}

// load target class using custom class loader
VerboseLoader loader =
new VerboseLoader(urls, base.getParent());
Class clas = loader.loadClass(args[0]);

// invoke "main" method of target class
Class[] ptypes =
new Class[] { args.getClass() };
Method main =
clas.getDeclaredMethod("main", ptypes);
String[] pargs = new String[args.length-1];
System.arraycopy(args, 1, pargs, 0, pargs.length);
Thread.currentThread().
setContextClassLoader(loader);
main.invoke(null, new Object[] { pargs });

} catch ...
}

} else {
System.out.println
("Usage: VerboseLoader main-class args...");
}
}
}

我们已从 java.net.URLClassLoader 派生了我们自己的 VerboseLoader 类,它列出正在被加载的所有类,同时指出哪些类是由这个 loader 实例(而不是委托父 classloader)加载的。这里同样为简洁起见而省略了导入和异常处理语句。

VerboseLoader 类中的前两个方法 loadClass()findClass() 重载了标准的 classloader 方法。 loadClass() 方法分别针对 classloader 请求的每个类作了调用。在此例中,我们仅让它向控制台打印一条消息,然后调用它的基类版本来执行实际的处理。基类方法实现了标准 classloader 委托行为,即首先检查父 classloader 是否能够加载所请求的类,并且仅在父 classloader 无法加载该类时,才尝试使用受保护的 findClass() 方法来直接加载该类。对于 findClass()VerboseLoader 实现,我们首先调用重载的基类实现,然后在调用成功(在没有抛出异常的情况下返回)时打印一条消息。

VerboseLoadermain() 方法或者从用于包含类的 loader 中获得 classpath URL 的列表,或者在与不属于 URLClassLoader 的实例的 loader 一起使用的情况下,简单地使用当前目录作为唯一的 classpath 条目。不管采用哪种方式,它都会列出实际正在使用的路径,然后创建 VerboseLoader 类的一个实例,并使用该实例来加载命令行上指定的目标类。该逻辑的其余部分(即查找和调用目标类的 main() 方法)与 清单 1 中的 Run 代码相同。

清单 3 显示了 VerboseLoader 命令行和输出的一个例子,它用于调用清单 1 中的 Run 应用程序:


清单 3. 清单 2 中的程序的例子输出


[dennis]$ java VerboseLoader Run
Loading from paths:
file:/home/dennis/writing/articles/devworks/dynamic/code5/
loadClass: Run
loadClass: java.lang.Object
findclass: loaded Run from this loader
loadClass: java.lang.Throwable
loadClass: java.lang.reflect.InvocationTargetException
loadClass: java.lang.IllegalAccessException
loadClass: java.lang.IllegalArgumentException
loadClass: java.lang.NoSuchMethodException
loadClass: java.lang.ClassNotFoundException
loadClass: java.lang.NoClassDefFoundError
loadClass: java.lang.Class
loadClass: java.lang.String
loadClass: java.lang.System
loadClass: java.io.PrintStream
Usage: Run main-class args...

在此例中,唯一直接由 VerboseLoader 加载的类是 Run 类。 Run 使用的其他所有类都是核心 Java 类,它们是通过父 classloader 使用委托来加载的。这其中的大多数(如果不是全部的话)核心类实际上都会在 VerboseLoader 应用程序本身的启动期间加载,因此父 classloader 将只返回一个指向先前创建的 java.lang.Class 实例的引用。

Javassist 拦截

清单 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 观察者实现仅只是打印一条关于正在被请求的类的消息。


清单 4. Javassist 应用程序运行器


public class JavassistRun
{
public static void main(String[] args) {
if (args.length >= 1) {
try {

// set up class loader with translator
Translator xlat = new VerboseTranslator();
ClassPool pool = ClassPool.getDefault(xlat);
Loader loader = new Loader(pool);

// invoke "main" method of target class
String[] pargs = new String[args.length-1];
System.arraycopy(args, 1, pargs, 0, pargs.length);
loader.run(args[0], pargs);

} catch ...
}

} else {
System.out.println
("Usage: JavassistRun main-class args...");
}
}

public static class VerboseTranslator implements Translator
{
public void start(ClassPool pool) {}

public void onWrite(ClassPool pool, String cname) {
System.out.println("onWrite called for " + cname);
}
}
}

下面是 JavassistRun 命令行和输出的一个例子,其中使用它来调用 清单 1 中的 Run 应用程序。


[dennis]$java -cp .:javassist.jar JavassistRun Run
onWrite called for Run
Usage: Run main-class args...





回页首


运行时定时

我们在 第 4 部分中 分析过的方法定时修改对于隔离性能问题来说可能一个很有用的工具,但它的确需要一个更灵活的接口。在该文中,我们只是将类和方法名称作为参数传递给程序, 程序加载二进制类文件,添加定时代码,然后写回该类。对于本文,我们将把代码转换为使用加载时修改方法,并将它转换为可支持模式匹配,用以指定要定时的类 和方法。

在加载类时更改代码以处理这种修改是很容易的。在清单 4 中的 javassist.Translator 代码的基础上,当正在写出的类名称与目标类名称匹配时,我们可以仅从 onWrite() 调用用于添加定时信息的方法。清单 5 展示了这一点(没有包含 addTiming() 的全部细节――请参阅第 4 部分以了解这些细节)。


清单 5. 在加载时添加定时代码


public class TranslateTiming
{
private static void addTiming(CtClass clas, String mname)
throws NotFoundException, CannotCompileException {
...
}

public static void main(String[] args) {
if (args.length >= 3) {
try {

// set up class loader with translator
Translator xlat =
new SimpleTranslator(args[0], args[1]);
ClassPool pool = ClassPool.getDefault(xlat);
Loader loader = new Loader(pool);

// invoke "main" method of target class
String[] pargs = new String[args.length-3];
System.arraycopy(args, 3, pargs, 0, pargs.length);
loader.run(args[2], pargs);

} catch (Throwable ex) {
ex.printStackTrace();
}

} else {
System.out.println("Usage: TranslateTiming" +
" class-name method-mname main-class args...");
}
}

public static class SimpleTranslator implements Translator
{
private String m_className;
private String m_methodName;

public SimpleTranslator(String cname, String mname) {
m_className = cname;
m_methodName = mname;
}

public void start(ClassPool pool) {}

public void onWrite(ClassPool pool, String cname)
throws NotFoundException, CannotCompileException {
if (cname.equals(m_className)) {
CtClass clas = pool.get(cname);
addTiming(clas, m_methodName);
}
}
}
}

模式方法

如清单 5 所示,除了使方法定时代码在加载时工作外,在指定要定时的方法时增加灵活性也是很理想的。我最初使用 Java 1.4 java.util.regex 包中的正则表达式匹配支持来实现这点,然后意识到它并没有真正带来我想要的那种灵活性。问题在于,用于选择要修改的类和方法的有意义的模式种类无法很好地适应正则表达式模型。

那么哪种模式对于选择类和方法 意 义呢?我想要的是在模式中使用类和方法的任何几个特征的能力,包括实际的类和方法名称、返回类型,以及调用参数类型。另一方面,我不需要对名称和类型进行 真正灵活的比较――简单的相等比较就能处理我感兴趣的大多数情况,而对该比较添加基本的通配符就能处理其余的所有情况了。处理这种情况的最容易方法是使模 式看起来像标准的 Java 方法声明,另外再进行一些扩展。

关于这种方法的例子,下面是几个与 test.StringBuilder 类的 String buildString(int) 方法相匹配的模式:


java.lang.String test.StringBuilder.buildString(int)
test.StringBuilder.buildString(int)
*buildString(int)
*buildString

这些模式的通用模式首先是一个可 选的返回类型(具有精确的文本),然后是组合起来的类和方法名称模式(具有“*”通配字符),最后是参数类型列表(具有精确的文本)。如果提供了返回类 型,必须使用一个空格将它与方法名称匹配相隔离,而参数列表则跟在方法名称匹配后面。为了使参数匹配更灵活,我通过两种方式来设置它。如果所给的参数是圆 括号括起的列表,它们必须精确匹配方法参数。如果它们是使用方括号(&ldquo;[]&rdquo;)来括起的,所列出的类型全都必须 作为匹配方法的参数来提供,不过该方法可以按任何顺序使用它们,并且还可以使用附加的参数。因此 *buildString(int, java.lang.String) 将匹配其名称以“buildString”结尾的任何方法,并且这些方法精确地按顺序接受一个 int 类型和一个 String 类型的参数。 *buildString[int,java.lang.String] 将匹配具有相同名称的方法,但是这些方法接受两个 或更多的 参数,其中一个是 int 类型,另一个是 java.lang.String 类型。

清单 6 给出了我编写来处理这些模式的 javassist.Translator 子类的简略版本。实际的匹配代码与本文并不真正相关,不过如果您想要查看它或亲自使用它,我已将它包括在了下载文件中(请参阅 参考资料)。使用这个 TimingTranslator 的主程序类是 BatchTiming ,它也包括在下载文件中。


清单 6. 模式匹配转换程序

    
public class TimingTranslator implements Translator
{
public TimingTranslator(String pattern) {
// build matching structures for supplied pattern
...
}

private boolean matchType(CtMethod meth) {
...
}

private boolean matchParameters(CtMethod meth) {
...
}

private boolean matchName(CtMethod meth) {
...
}

private void addTiming(CtMethod meth) {
...
}

public void start(ClassPool pool) {}

public void onWrite(ClassPool pool, String cname)
throws NotFoundException, CannotCompileException {

// loop through all methods declared in class
CtClass clas = pool.get(cname);
CtMethod[] meths = clas.getDeclaredMethods();
for (int i = 0; i < meths.length; i++) {

// check if method matches full pattern
CtMethod meth = meths[i];
if (matchType(meth) &&

matchParameters(meth) && matchName(meth)) {

// handle the actual timing modification
addTiming(meth);
}
}
}
}





回页首


后续内容

在上两篇文章中,您已经看到了如何使用 Javassist 来处理基本的转换。对于下一篇文章,我们将探讨这个框架的高级特性,这些特性提供用于编辑字节代码的查找和替换技术。这些特性使得对程序行为做出系统性的 变更很容易,其中包括诸如拦截所有方法调用或所有字段访问这样的变更。它们是理解为什么 Javassist 是 Java 程序中提供面向方面支持的卓越框架的关键。请下个月再回来看看如何能够使用 Javassist 来揭示应用程序中的方面(aspect)。

 

分享到:
评论

相关推荐

    Java经典编程源码基础例程300.zip

    Java经典编程源码例程,可以做为你的学习设计参考。 第1章 Java语言概述 1 实例001 输出“Hello World” 2 实例002 输出控制台传递的参数 2 实例003 输出由“*”组成的三角形 3 实例004 输出符号表情 5 第2章 ...

    Java开发详解.zip

    031505_【第15章:Java反射机制】_动态代理笔记.pdf 031506_【第15章:Java反射机制】_工厂设计模式笔记.pdf 031601_【第16章:Annotation】_系统内建Annotation笔记.pdf 031602_【第16章:Annotation】_自定义...

    java 编程入门思考

    附录C Java编程规则 附录D 性能 D.1 基本方法 D.2 寻找瓶颈 D.2.1 安插自己的测试代码 D.2.2 JDK性能评测[2] D.2.3 特殊工具 D.2.4 性能评测的技巧 D.3 提速方法 D.3.1 常规手段 D.3.2 依赖语言的方法 D.3.3 特殊...

    JAVA_API1.6文档(中文)

    java.lang 提供利用 Java 编程语言进行程序设计的基础类。 java.lang.annotation 为 Java 编程语言注释设施提供库支持。 java.lang.instrument 提供允许 Java 编程语言代理检测运行在 JVM 上的程序的服务。 java....

    Java开发技术大全(500个源代码).

    loadClass.java 动态装载一个类 Shape.java 一个用于测试的简单类 useArray1.java 用反射机制使用数组示例1 useArray2.java 用反射机制使用数组示例2 第10章 示例描述:本章学习泛型。 demoBounds.java 演示有...

    Thinking in Java 中文第四版+习题答案

    附录C Java编程规则 附录D 性能 D.1 基本方法 D.2 寻找瓶颈 D.2.1 安插自己的测试代码 D.2.2 JDK性能评测 D.2.3 特殊工具 D.2.4 性能评测的技巧 D.3 提速方法 D.3.1 常规手段 D.3.2 依赖语言的方法 D.3.3 特殊情况 D...

    Java数据库编程宝典3

    第5章 使用JDBC和SQL创建表 5.1 创建数据库 5.2 使用表 5.2.1 记录和字段、行和列 5.2.2 SQL数据类型 5.2.3 完整性约束 5.3 创建表 5.4 使用JDBC创建表 5.4.1 DriverManager 5.4.2 驱动程序 5.4.3 连接 ...

    Java 1.6 API 中文 New

    java.lang 提供利用 Java 编程语言进行程序设计的基础类。 java.lang.annotation 为 Java 编程语言注释设施提供库支持。 java.lang.instrument 提供允许 Java 编程语言代理检测运行在 JVM 上的程序的服务。 java....

    java源码包---java 源码 大量 实例

     Java数据压缩与传输实例,可以学习一下实例化套按字、得到文件输入流、压缩输入流、文件输出流、实例化缓冲区、写入数据到文件、关闭输入流、关闭套接字关闭输出流、输出错误信息等Java编程小技巧。 Java数组倒置...

    java api最新7.0

    java.lang 提供利用 Java 编程语言进行程序设计的基础类。 java.lang.annotation 为 Java 编程语言注释设施提供库支持。 java.lang.instrument 提供允许 Java 编程语言代理检测运行在 JVM 上的程序的服务。 java....

    java源码包2

     Java数据压缩与传输实例,可以学习一下实例化套按字、得到文件输入流、压缩输入流、文件输出流、实例化缓冲区、写入数据到文件、关闭输入流、关闭套接字关闭输出流、输出错误信息等Java编程小技巧。 Java数组倒置...

    JavaAPI1.6中文chm文档 part1

    由于比较大分为两个部分,这是第一部分 java.applet 提供创建 applet 所必需的类和 applet 用来与其 applet 上下文通信的类。 java.awt 包含用于创建用户界面和绘制图形图像的所有类。 java.awt.color 提供用于...

    java开源包5

    ftp4j是一个FTP客户端Java类库,实现了FTP客户端应具有的大部分功能文件(包括上传和下 载),浏览远程FTP服务器上的目录和文件,创建、删除、重命,移动远程目录和文件。ftp4j提供多种方式连接到远程FTP服务器包括...

    java源码包4

     Java数据压缩与传输实例,可以学习一下实例化套按字、得到文件输入流、压缩输入流、文件输出流、实例化缓冲区、写入数据到文件、关闭输入流、关闭套接字关闭输出流、输出错误信息等Java编程小技巧。 Java数组倒置...

    java源码包3

     Java数据压缩与传输实例,可以学习一下实例化套按字、得到文件输入流、压缩输入流、文件输出流、实例化缓冲区、写入数据到文件、关闭输入流、关闭套接字关闭输出流、输出错误信息等Java编程小技巧。 Java数组倒置...

    疯狂JAVA讲义

    学生提问:老师,我想学习Java编程,到底是学习Eclipse好呢,还是学习JBuilder好呢? 21 1.9 本章小结 22 本章练习 22 第2章 理解面向对象 23 2.1 面向对象 24 2.1.1 结构化程序设计简介 24 2.1.2 程序的三种...

    JAVA上百实例源码以及开源项目

     Java数据压缩与传输实例,可以学习一下实例化套按字、得到文件输入流、压缩输入流、文件输出流、实例化缓冲区、写入数据到文件、关闭输入流、关闭套接字关闭输出流、输出错误信息等Java编程小技巧。 Java数组倒置...

    C++编程思想1-5 清晰PDF

    内容涉及对象的演化、数据抽象、隐藏实现、初始化与清除、函数重载与缺省参数、输入输出流介绍、常量、内联函数、命名控制、引用和拷贝构造函数、运算符重载、动态对象创建、继承和组合、多态和虚函数、模板和包容器...

Global site tag (gtag.js) - Google Analytics