Java 编程的动态性,第 7 部分: 用 BCEL 设计字节码--转载
在本系列的最后三篇文章中,我展示了如何用 Javassist 框架操作类。这次我将用一种很不同的方法操纵字节码——使用 Apache Byte Code Engineering Library (BCEL)。与 Javassist 所支持的源代码接口不同,BCEL 在实际的 JVM 指令层次上进行操作。在希望对程序执行的每一步进行控制时,底层方法使 BCEL 很有用,但是当两者都可以胜任时,它也使 BCEL 的使用比 Javassist 要复杂得多。 我将首先讨论 BCEL 基本体系结构,然后本文的大部分内容将讨论用 BCEL 重新构建我的第一个 Javassist 类操作的例子。最后简要介绍 BCEL 包中提供的一些工具和开发人员用 BCEL 构建的一些应用程序。 BCEL 使您能够同样具备 Javassist 提供的分析、编辑和创建 Java 二进制类的所有基本能力。BCEL 的一个明显区别是每项内容都设计为在 JVM 汇编语言的级别、而不是 Javassist 所提供的源代码接口上工作。除了表面上的差别,还有一些更深层的区别,包括在 BCEL 中组件的两个不同层次结构的使用——一个用于检查现有的代码,另一个用于创建新代码。我假定读者已经通过本系列前面的文章熟悉了 Javassist(请参阅侧栏?)。 因此我将主要介绍在开始使用 BCEL 时,可能会让您感到迷惑的那些不同之处。 与 Javassist 一样, BCEL 在类分析方面的功能基本上与 Java 平台通过 Relfection API 直接提供的功能是重复的。这种重复对于类操作工具箱来说是必要的,因为一般不希望在所要操作的类被修改?之前就装载它们。 BCEL 在?
除了对类组件的反射形式的访问,? 就? 听起来有些乱?我想是的。事实上,在两个包之间来回转是使用 BCEL 的一个最主要的缺点。重复的类结构总有些碍手碍脚,所以如果频繁使用 BCEL,那么可能需要编写一个包装器类,它可以隐藏其中一些不同之处。在本文中,我将主要使用? 除了?
作为使用 BCEl 的一个例子,我将使用?中的一个 Javassist 例子——测量执行一个方法的时间。我甚至采用了与使用 Javassist 时的相同方式:用一个改过的名字创建要计时的原方法的一个副本,然后,通过调用改名后的方法,利用包装了时间计算的代码来替换原方法的主体。 清单 1 给出了一个用于展示目的示例方法:? 清单 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());
}
}
} 清单 2 显示了等同于用 BCEL 进行类操作改变的源代码。这里包装器方法只是保存当前时间,然后调用改名后的原方法,并在返回调用原方法的结果之前打印时间报告。 清单 2. 在原方法中加入计时 private String buildString(int length) {
long start = System.currentTimeMillis();
String result = buildString$impl(length);
System.out.println("Call to buildString$impl took " +
(System.currentTimeMillis()-start) + " ms.");
return result;
}
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());
}
}
} 用我在?一节中描述的 BCEL API 实现添加方法计时的代码。在 JVM 指令级别上的操作使代码比??中 Javassist 的例子要长得多,所以这里我准备在提供完整的实现之前,一段一段地介绍。在最后的代码中,所有片段构成一个方法,它有两个参数:? 清单 3 是转换方法的第一段代码。可以从注释中看到,第一部分只是初始化要使用的基本 BCEL 组件,它包括用要计时方法的信息初始化一个新的? 清单 3. 添加拦截方法 // rename a copy of the original method
MethodGen methgen = new MethodGen(method,pgen); cgen.removeMethod(method); String iname = methgen.getName() + "$impl"; methgen.setName(iname); cgen.addMethod(methgen.getMethod()); 清单 4 给出了转换方法的下一段代码。这里的第一部分计算方法调用参数在堆栈上占用的空间。之所以需要这段代码,是因为为了在调用包装方法之前在堆栈帧上存储开始时间,我需要知道局部变量可以使用什么偏移值(注意,我可以用 BCEL 的局部变量处理得到同样的效果,但是在本文中我选择使用显式的方式)。这段代码的第二部分生成对? 您可能会奇怪为什么在开始参数大小计算时要检查方法是否是静态的,如果是静态的,将堆栈帧槽初始化为零(不是静态正好相反)。这种方式与 Java 如何处理方法调用有关。对于非静态的方法,每次调用的第一个(隐藏的)参数是目标对象的? 清单 4. 设置包装的调用 // save time prior to invocation
ilist.append(ifact.createInvoke("java.lang.System","currentTimeMillis",Type.LONG,Type.NO_ARGS,Constants.INVOKESTATIC)); ilist.append(InstructionFactory.createStore(Type.LONG,slot)); 清单 5 显示了生成对包装方法的调用并保存结果(如果有的话)的代码。这段代码的第一部分再次检查方法是否是静态的。如果方法不是静态的,那么就生成将? 清单 5. 调用包装的方法 // store result for return later
if (result != Type.VOID) { ilist.append(InstructionFactory.createStore(result,slot+2)); } 现在开始包装。清单 6 生成实际计算开始时间后经过的毫秒数,并作为编排好格式的消息打印出来的代码。这一部分看上去很复杂,但是大多数操作实际上只是写出输出消息的各个部分。它确实展示了几种我在前面的代码中没有使用的操作类型,包括字段访问(到 清单 6. 计算并打印所使用的时间 生成了计时消息代码后,留给清单 7 的就是保存包装的方法的调用结果值(如果有的话),然后结束构建的包装器方法。最后这部分涉及几个步骤。调用? 清单 7. 完成包装器 // finalize the constructed method
wrapgen.stripAttributes(true); wrapgen.setMaxStack(); wrapgen.setMaxLocals(); cgen.addMethod(wrapgen.getMethod()); ilist.dispose(); 清单 8 显示了完整的代码(稍微改变了一下格式以适合显示宽度),包括以类文件的名字为参数的? 清单 8. 完整的转换代码 // set up the construction tools
InstructionFactory ifact = new InstructionFactory(cgen);
InstructionList ilist = new InstructionList();
ConstantPoolGen pgen = cgen.getConstantPool();
String cname = cgen.getClassName();
MethodGen wrapgen = new MethodGen(method,pgen);
wrapgen.setInstructionList(ilist);
// rename a copy of the original method
MethodGen methgen = new MethodGen(method,pgen);
cgen.removeMethod(method);
String iname = methgen.getName() + "$impl";
methgen.setName(iname);
cgen.addMethod(methgen.getMethod());
Type result = methgen.getReturnType();
// compute the size of the calling parameters
Type[] types = methgen.getArgumentTypes();
int slot = methgen.isStatic() ? 0 : 1;
for (int i = 0; i < types.length; i++) {
slot += types[i].getSize();
}
// save time prior to invocation
ilist.append(ifact.createInvoke("java.lang.System",Constants.INVOKESTATIC));
ilist.append(InstructionFactory.
createStore(Type.LONG,slot));
// call the wrapped method
int offset = 0;
short invoke = Constants.INVOKESTATIC;
if (!methgen.isStatic()) {
ilist.append(InstructionFactory.
createLoad(Type.OBJECT,0));
offset = 1;
invoke = Constants.INVOKEVIRTUAL;
}
for (int i = 0; i < types.length; i++) {
Type type = types[i];
ilist.append(InstructionFactory.
createLoad(type,offset));
offset += type.getSize();
}
ilist.append(ifact.createInvoke(cname,invoke));
// store result for return later
if (result != Type.VOID) {
ilist.append(InstructionFactory.
createStore(result,slot+2));
}
// print time required for method call
ilist.append(ifact.createFieldAccess("java.lang.System",Constants.GETSTATIC));
ilist.append(InstructionConstants.DUP);
ilist.append(InstructionConstants.DUP);
String text = "Call to method " + methgen.getName() +
" took ";
ilist.append(new PUSH(pgen,text));
ilist.append(ifact.createInvoke("java.io.PrintStream",Constants.INVOKEVIRTUAL));
ilist.append(ifact.createInvoke("java.lang.System",Constants.INVOKESTATIC));
ilist.append(InstructionFactory.
createLoad(Type.LONG,slot));
ilist.append(InstructionConstants.LSUB);
ilist.append(ifact.createInvoke("java.io.PrintStream",Constants.INVOKEVIRTUAL));
ilist.append(new PUSH(pgen," ms."));
ilist.append(ifact.createInvoke("java.io.PrintStream",Constants.INVOKEVIRTUAL));
// return result from wrapped method call
if (result != Type.VOID) {
ilist.append(InstructionFactory.
createLoad(result,slot+2));
}
ilist.append(InstructionFactory.createReturn(result));
// finalize the constructed method
wrapgen.stripAttributes(true);
wrapgen.setMaxStack();
wrapgen.setMaxLocals();
cgen.addMethod(wrapgen.getMethod());
ilist.dispose();
}
public static void main(String[] argv) {
if (argv.length == 2 && argv[0].endsWith(".class")) {
try {
JavaClass jclas = new ClassParser(argv[0]).parse();
ClassGen cgen = new ClassGen(jclas);
Method[] methods = jclas.getMethods();
int index;
for (index = 0; index < methods.length; index++) {
if (methods[index].getName().equals(argv[1])) {
break;
}
}
if (index < methods.length) {
addWrapper(cgen,methods[index]);
FileOutputStream fos =
new FileOutputStream(argv[0]);
cgen.getJavaClass().dump(fos);
fos.close();
} else {
System.err.println("Method " + argv[1] +
" not found in " + argv[0]);
}
} catch (IOException ex) {
ex.printStackTrace(System.err);
}
} else {
System.out.println
("Usage: BCELTiming class-file method-name");
}
}
} 清单 9 显示了以未修改形式第一次运行? 清单 9. 运行这个程序
BCEL 有比我在本文中所介绍的基本类操作支持更多的功能。它还包括完整的验证器实现以保证二进制类对于 JVM 规范是有效的(参见 我自己使用 BCEL 时,发现 HTML 反汇编程序特别有用。要想试用它,只要执行 BCEL JAR 中的? 图 1 是反汇编程序生成的分帧输出的屏幕快照。在这个快照中,右上角的大帧显示了添加到? 当前,BCEL 可能是 Java 类操作使用最多的框架。在 Web 网站上列出了一些使用 BCEL 的其他项目,包括 Xalan XSLT 编译器、Java 编程语言的 AspectJ 扩展和几个 JDO 实现。许多其他未列出的项目也使用 BCEL,包括我自己的 JiBX XML 数据绑定项目。不过,BCEL 列出的几个项目已经转而使用其他库,所以不要将这个列表作为 BCEL 大众化程度的绝对依据。 BCEL 最大的好处是它的商业友好的 Apache 许可证及其丰富的 JVM 指令级支持。这些功能结合其稳定性和长寿性,使它成为类操作应用程序的非常流行的选择。不过,BCEL 看来没有设计为具有很好的速度或者容易使用。在大多数情况下,Javassist 提供了更友好的 API,并有相近的速度(甚至更快),至少在我的简单测试中是这样。如果您的项目可以使用 Mozilla Public License (MPL) 或者 GNU Lesser General Public License (LGPL),那么 Javassist 可能是更好的选择(它在这两种许可证下都可以用)。
我已经介绍了 Javassist 和 BCEL,本系列的下一篇文章将深入比我们目前已经介绍的用途更大的类操作应用程序。在?,我展示了方法调用反射比直接调用慢得多。在第 8 部分中,我将显示如何使用 Javassist 和 BCEL,以便用运行时动态生成的代码替换反射调用,从而极大地提高性能。下个月请回来看另一篇?Java 编程的动态性以了解详情。 原文:http://www.ibm.com/developerworks/cn/java/j-dyn0414/index.html (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |