Java 编程的动态性, 第4部分: 用 Javassist 进行类转换--转载
讲过了 Java 类格式和利用反射进行的运行时访问后,本系列到了进入更高级主题的时候了。本月我将开始本系列的第二部分,在这里 Java 类信息只不过是由应用程序操纵的另一种形式的数据结构而已。我将这个主题的整个内容称为?classworking。 我将以 Javassist 字节码操作库作为对 classworking 的讨论的开始。Javassist 不仅是一个处理字节码的库,而且更因为它的另一项功能使得它成为试验 classworking 的很好的起点。这一项功能就是:可以用 Javassist 改变 Java 类的字节码,而无需真正了解关于字节码或者 Java 虚拟机(Java virtual machine JVM)结构的任何内容。从某方面将这一功能有好处也有坏处 -- 我一般不提倡随便使用不了解的技术 -- 但是比起在单条指令水平上工作的框架,它确实使字节码操作更可具有可行性了。 Javassist 使您可以检查、编辑以及创建 Java 二进制类。检查方面基本上与通过 Reflection API 直接在 Java 中进行的一样,但是当想要修改类而不只是执行它们时,则另一种访问这些信息的方法就很有用了。这是因为 JVM 设计上并没有提供在类装载到 JVM 中后访问原始类数据的任何方法,这项工作需要在 JVM 之外完成。 Javassist 使用? 装载到类池中的类由? 字段、方法和构造函数分别由? 所有字节码的源代码?Javassist 让您可以完全替换一个方法或者构造函数的字节码正文,或者在现有正文的开始或者结束位置选择性地添加字节码(以及在构造函数中添加其他一些变量)。不管是哪种情况,新的字节码都作为类 Java 的源代码声明或者? Javassist 接受的源代码与 Java 语言的并不完全一致,不过主要的区别只是增加了一些特殊的标识符,用于表示方法或者构造函数参数、方法返回值和其他在插入的代码中可能用到的内容。这些特殊标识符以符号? 对于在传递给 Javassist 的源代码中可以做的事情有一些限制。第一项限制是使用的格式,它必须是单条语句或者块。在大多数情况下这算不上是限制,因为可以将所需要的任何语句序列放到块中。下面是一个使用特殊 Javassist 标识符表示方法中前两个参数的例子,这个例子用来展示其使用方法: 对于源代码的一项更实质性的限制是不能引用在所添加的声明或者块外声明的局部变量。这意味着如果在方法开始和结尾处都添加了代码,那么一般不能将在开始处添加的代码中的信息传递给在结尾处添加的代码。有可能绕过这项限制,但是绕过是很复杂的 -- 通常需要设法将分别插入的代码合并为一个块。
作为使用 Javassist 的一个例子,我将使用一个通常直接在源代码中处理的任务:测量执行一个方法所花费的时间。这在源代码中可以容易地完成,只要在方法开始时记录当前时间、之后在方法结束时再次检查当前时间并计算两个值的差。如果没有源代码,那么得到这种计时信息就要困难得多。这就是 classworking 方便的地方 -- 它让您对任何方法都可以作这种改变,并且不需要有源代码。 清单 1 显示了一个(不好的)示例方法,我用它作为我的计时试验的实验品:?
public static void main(String[] argv) {
StringBuilder inst = new StringBuilder();
for (int i = 0; i < argv.length; i++) {
String result = inst.buildString(Integer.parseInt(argv[i]));
System.out.println("Constructed string of length " +
result.length());
}
}
} 因为有这个方法的源代码,所以我将为您展示如何直接添加计时信息。它也作为使用 Javassist 时的一个模型。清单 2 只展示了 来做 使用 Javassist 操作类字节码以得到同样的效果看起来应该不难。Javassist 提供了在方法的开始和结束位置添加代码的方法,别忘了,我在为该方法中加入计时信息就是这么做的。 不过,还是有障碍。在描述 Javassist 是如何让您添加代码时,我提到添加的代码不能引用在方法中其他地方定义的局部变量。这种限制使我不能在 Javassist 中使用在源代码中使用的同样方法实现计时代码,在这种情况下,我在开始时添加的代码中定义了一个新的局部变量,并在结束处添加的代码中引用这个变量。 那么还有其他方法可以得到同样的效果吗?是的,我?可以在类中添加一个新的成员字段,并使用这个字段而不是局部变量。不过,这是一种糟糕的解决方案,在一般性的使用中有一些限制。例如,考虑在一个递归方法中会发生的事情。每次方法调用自身时,上次保存的开始时间值就会被覆盖并且丢失。 幸运的是有一种更简洁的解决方案。我可以保持原来方法的代码不变,只改变方法名,然后用原来的方法名增加一个新方法。这个?拦截器(interceptor)方法可以使用与原来方法同样的签名,包括返回同样的值。清单 3 展示了通过这种方法改编后源代码看上去的样子: 通过 Javassist 可以很好地利用这种使用拦截器方法的方法。因为整个方法是一个块,所以我可以毫无问题地在正文中定义并且使用局部变量。为拦截器方法生成源代码也很容易 -- 对于任何可能的方法,只需要几个替换。 实现添加方法计时的代码要用到在?中描述的一些 Javassist API。清单 4 展示了该代码,它是一个带有两个命令行参数的应用程序,这两个参数分别给出类名和要计时的方法名。?
// start by getting the class file and method
CtClass clas = ClassPool.getDefault().get(argv[0]);
if (clas == null) {
System.err.println("Class " + argv[0] + " not found");
} else {
// add timing interceptor to the class
addTiming(clas,argv[1]);
clas.writeFile();
System.out.println("Added timing to method " +
argv[0] + "." + argv[1]);
}
} catch (CannotCompileException ex) {
ex.printStackTrace();
} catch (NotFoundException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
}
} else {
System.out.println("Usage: JassistTiming class method-name");
}
}
private static void addTiming(CtClass clas,String mname)
throws NotFoundException,CannotCompileException {
// get the method information (throws exception if method with
// given name is not declared directly by this class,returns
// arbitrary choice if more than one with the given name)
CtMethod mold = clas.getDeclaredMethod(mname);
// rename old method to synthetic name,then duplicate the
// method with original name for use as interceptor
String nname = mname+"$impl";
mold.setName(nname);
CtMethod mnew = CtNewMethod.copy(mold,mname,clas,null);
// start the body text generation by saving the start time
// to a local variable,then call the timed method; the
// actual code generated needs to depend on whether the
// timed method returns a value
String type = mold.getReturnType().getName();
StringBuffer body = new StringBuffer();
body.append("{nlong start = System.currentTimeMillis();n");
if (!"void".equals(type)) {
body.append(type + " result = ");
}
body.append(nname + "($$);n");
// finish body text generation with call to print the timing
// information,and return saved value (if not void)
body.append("System.out.println("Call to method " + mname +
" took " +n (System.currentTimeMillis()-start) + " +
"" ms.");n");
if (!"void".equals(type)) {
body.append("return result;n");
}
body.append("}");
// replace the body of the interceptor method with generated
// code block and add it to class
mnew.setBody(body.toString());
clas.addMethod(mnew);
// print the generated code block just to show what was done
System.out.println("Interceptor method body:");
System.out.println(body.toString());
}
} 构造拦截器方法的正文时使用一个? 除了对(重命名的)原来方法的调用,实际的正文内容看起来就像标准的 Java 代码。它是代码中的? 清单 5 展示了首先运行未修改过的?
Javassist 通过让您处理源代码而不是实际的字节码指令清单而使 classworking 变得容易。但是这种方便性也有一个缺点。正如我在?中提到的,Javassist 所使用的源代码与 Java 语言并不完全一样。除了在代码中识别特殊的标识符外,Javassist 还实现了比 Java 语言规范所要求的更宽松的编译时代码检查。因此,如果不小心,就会从源代码中生成可能会产生令人感到意外的结果的字节码。 作为一个例子,清单 6 展示了在将方法开始时的拦截器代码所使用的局部变量的类型从? long?储存到一个?
|