已阅读5页,还剩10页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
Apache BCEL 让您深入 JVM 汇编语言进行类操作的细节Dennis M. Sosnoski (), 总裁, Sosnoski Software Solutions, Inc.Dennis Sosnoski( )是西雅图地区的 Java 咨询公司 Sosnoski Software Solutions, Inc.的创始人和首席顾问。他有 30 多年的专业软件开发经验,最近几年致力于服务器端的 Java 技术,包括 servlet、Enterprise JavaBeans 和 XML。他经常在全国性的会议上就 Java 中的 XML 和 J2EE 技术发表言论。简介:Apache Byte Code Engineering Library (BCEL)可以深入 Java 类的字节码。可以用它转换现有的类表示或者构建新的类,因为 BCEL 在单独的 JVM 指令级别上进行操作,所以可以让您对代码有最强大的控制。不过,这种能力的代价是复杂性。在本文中,Java 顾问 Dennis Sosnoski 介绍了 BCEL 的基本内容,并引导读者完成一个示例 BCEL 应用程序,这样您就可以自己决定是否值得以这种复杂性来换取这种能力。标记本文!发布日期:2004 年 4 月 01 日 级别:初级 访问情况 :4408 次浏览 评论:0(查看|添加评论 - 登录) 平均分 (8个评分)为本文评分在本系列的最后三篇文章中,我展示了如何用 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 直接提供的功能是重复的。这种重复对于类操作工具箱来说是必要的,因为一般不希望在所要操作的类被修改 之前就装载它们。 不要错过本系列的其余部分第 1 部分: “ 类和类装入”(2003 年 4 月) 第 2 部分,“ 引入反射” (2003 年 6 月) 第 3 部分,“ 应用反射” (2003 年 7 月) 第 4 部分, “ 用 Javassist 进行类转换” (2003 年 9 月) 第 5 部分, “ 动态转换类” (2004 年 2 月) 第 6 部分,“ 用 Javassist 进行面向方面的更改” (2004 年 3 月) 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 时的相同方式:用一个改过的名字创建要计时的原方法的一个副本,然后,通过调用改名后的方法,利用包装了时间计算的代码来替换原方法的主体。 选择一个试验品清单 1 给出了一个用于展示目的示例方法: StringBuilder 类的 buildString 方法。正如我在 第 4 部分所说的,这个方法采用了所有 Java 性能专家告诫您 不要 使用的方式来构建一个 String 它重复地在字符串的未尾附加单个字符以创建更长的字符串。因为字符串是不可变的,所以这种方式意味着每次循环时会构建一个新的字符串,从老的字符串拷贝数据并在最后增加一个字符。总的效果就是用这个方法创建更长的字符串时,它会产生越来越大的开销。 清单 1. 要计时的方法 public class StringBuilder private String buildString(int length) String result = ; for (int i = 0; i length; i+) result += (char)(i%26 + a); 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(argvi); System.out.println(Constructed string of length + result.length(); 清单 2 显示了等同于用 BCEL 进行类操作改变的源代码。这里包装器方法只是保存当前时间,然后调用改名后的原方法,并在返回调用原方法的结果之前打印时间报告。清单 2. 在原方法中加入计时 public class StringBuilder private String buildString$impl(int length) String result = ; for (int i = 0; i length; i+) result += (char)(i%26 + a); return result; 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(argvi); System.out.println(Constructed string of length + result.length(); 编写转换代码用我在 BCEL 类访问一节中描述的 BCEL API 实现添加方法计时的代码。在 JVM 指令级别上的操作使代码比 第 4 部分 中 Javassist 的例子要长得多,所以这里我准备在提供完整的实现之前,一段一段地介绍。在最后的代码中,所有片段构成一个方法,它有两个参数: cgen 它是 org.apache.bcel.generic.ClassGen 类的一个实例,用被修改的类的现有信息初始化,和方法要计时方法的 org.apache.bcel.classfile.Method 实例。 清单 3 是转换方法的第一段代码。可以从注释中看到,第一部分只是初始化要使用的基本 BCEL 组件,它包括用要计时方法的信息初始化一个新的 org.apache.bcel.generic.MethodGen 实例。我为这个 MethodGen 设置一个空的指令清单,在后面我将用实际的计时代码填充它。在第 2 部分,我用原来的方法创建第二个 org.apache.bcel.generic.MethodGen 实例,然后从类中删除原来的方法。在第二个 MethodGen 实例中,我只是让名字加上“$impl”后缀,然后调用 getMethod() 以将可修改的方法信息转换为固定形式的 org.apache.bcel.classfile.Method 实例。然后调用 addMethod() 以便在类中添加改名后的方法。 清单 3. 添加拦截方法 / set up the construction toolsInstructionFactory ifact = new InstructionFactory(cgen);InstructionList ilist = new InstructionList();ConstantPoolGen pgen = cgen.getConstantPool();String cname = cgen.getClassName();MethodGen wrapgen = new MethodGen(method, cname, pgen);wrapgen.setInstructionList(ilist); / rename a copy of the original methodMethodGen methgen = new MethodGen(method, cname, pgen);cgen.removeMethod(method);String iname = methgen.getName() + $impl;methgen.setName(iname);cgen.addMethod(methgen.getMethod();清单 4 给出了转换方法的下一段代码。这里的第一部分计算方法调用参数在堆栈上占用的空间。之所以需要这段代码,是因为为了在调用包装方法之前在堆栈帧上存储开始时间,我需要知道局部变量可以使用什么偏移值(注意,我可以用 BCEL 的局部变量处理得到同样的效果,但是在本文中我选择使用显式的方式)。这段代码的第二部分生成对 java.lang.System.currentTimeMillis() 的调用,以得到开始时间,并将它保存到堆栈帧中计算出的局部变量偏移处。 您可能会奇怪为什么在开始参数大小计算时要检查方法是否是静态的,如果是静态的,将堆栈帧槽初始化为零(不是静态正好相反)。这种方式与 Java 如何处理方法调用有关。对于非静态的方法,每次调用的第一个(隐藏的)参数是目标对象的 this 引用,在计算堆栈帧中完整参数集大小时我要考虑到这点。 清单 4. 设置包装的调用 / compute the size of the calling parametersType types = methgen.getArgumentTypes();int slot = methgen.isStatic() ? 0 : 1;for (int i = 0; i types.length; i+) slot += typesi.getSize(); / save time prior to invocationilist.append(ifact.createInvoke(java.lang.System, currentTimeMillis, Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC);ilist.append(InstructionFactory.createStore(Type.LONG, slot);清单 5 显示了生成对包装方法的调用并保存结果(如果有的话)的代码。这段代码的第一部分再次检查方法是否是静态的。如果方法不是静态的,那么就生成将 this 对象引用装载到堆栈中的代码,同时设置方法调用类型为 virtual (而不是 static )。然后 for 循环生成将所有调用参数值拷贝到堆栈中的代码, createInvoke() 方法生成对包装的方法的实际调用,最后 if 语句将结果值保存到位于堆栈帧中的另一个局部变量中(如果结果类型不是 void )。 清单 5. 调用包装的方法 / call the wrapped methodint 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 = typesi; ilist.append(InstructionFactory.createLoad(type, offset); offset += type.getSize();Type result = methgen.getReturnType();ilist.append(ifact.createInvoke(cname, iname, result, types, invoke); / store result for return laterif (result != Type.VOID) ilist.append(InstructionFactory.createStore(result, slot+2);现在开始包装。清单 6 生成实际计算开始时间后经过的毫秒数,并作为编排好格式的消息打印出来的代码。这一部分看上去很复杂,但是大多数操作实际上只是写出输出消息的各个部分。它确实展示了几种我在前面的代码中没有使用的操作类型,包括字段访问(到 java.lang.System.out )和几种不同的指令类型。如果将 JVM 想象为基于堆栈的处理器,则其中大多数是容易理解的,因此我在这里就不再详细说明了。 清单 6. 计算并打印所使用的时间 / print time required for method callilist.append(ifact.createFieldAccess(java.lang.System, out, new ObjectType(java.io.PrintStream), 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, print, Type.VOID, new Type Type.STRING , Constants.INVOKEVIRTUAL);ilist.append(ifact.createInvoke(java.lang.System, currentTimeMillis, Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC);ilist.append(InstructionFactory.createLoad(Type.LONG, slot);ilist.append(InstructionConstants.LSUB);ilist.append(ifact.createInvoke(java.io.PrintStream, print, Type.VOID, new Type Type.LONG , Constants.INVOKEVIRTUAL);ilist.append(new PUSH(pgen, ms.);ilist.append(ifact.createInvoke(java.io.PrintStream, println, Type.VOID, new Type Type.STRING , Constants.INVOKEVIRTUAL);生成了计时消息代码后,留给清单 7 的就是保存包装的方法的调用结果值(如果有的话),然后结束构建的包装器方法。最后这部分涉及几个步骤。调用 stripAttributes(true) 只是告诉 BCEL 不对构建的方法生成调试信息,而 setMaxStack() 和 setMaxLocals() 调用计算并设置方法的堆栈使用信息。完成了这一步后,就可以实际生成方法的最终版本,并将它加入到类中。 清单 7. 完成包装器 / return result from wrapped method callif (result != Type.VOID) ilist.append(InstructionFactory.createLoad(result, slot+2);ilist.append(InstructionFactory.createReturn(result); / finalize the constructed methodwrapgen.stripAttributes(true);wrapgen.setMaxStack();wrapgen.setMaxLocals();cgen.addMethod(wrapgen.getMethod();ilist.dispose();完整的代码清单 8 显示了完整的代码(稍微改变了一下格式以适合显示宽度),包括以类文件的名字为参数的 main() 方法和要转换的方法: 清单 8. 完整的转换代码 public class BCELTiming private static void addWrapper(ClassGen cgen, Method method) / 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, cname, pgen); wrapgen.setInstructionList(ilist); / rename a copy of the original method MethodGen methgen = new MethodGen(method, cname, 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 += typesi.getSize(); / 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); / 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 = typesi; ilist.append(InstructionFactory. createLoad(type, offset); offset += type.getSize(); ilist.append(ifact.createInvoke(cname, iname, result, types, 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, out, new ObjectType(java.io.PrintStream), 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, print, Type.VOID, new Type Type.STRING , Constants.INVOKEVIRTUAL); ilist.append(ifact.createInvoke(java.lang.System, currentTimeMillis, Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC); ilist.append(InstructionFactory. createLoad(Type.LONG, slot); ilist.append(InstructionConstants.LSUB); ilist.append(ifact.createInvoke(java.io.PrintStream, print, Type.VOID, new Type Type.LONG , Constants.INVOKEVIRTUAL); ilist.append(new PUSH(pgen, ms.); ilist.append(ifact.createInvoke(java.io.PrintStream, println, Type.VOID, new Type Type.STRING , 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 & argv0.endsWith(.class) try JavaClass jclas = new ClassParser(argv0).parse(); ClassGen cgen = new ClassGen(jclas); Method methods = jclas.getMethods(); int index; for (index = 0; index methods.length; index+) if (methodsindex.getName().equals(argv1) break; if (index methods.length) addWrapper(cgen, methodsindex); FileOutputStream fos = new FileOutputStream(argv0); cgen.getJavaClass().dump(fos); fos.close(); else System.err.println(Method + argv1 + not found in + argv0); catch (IOException ex) ex.printStackTrace(System.err); else System.out.println (Usage: BCELTiming class-file method-name); 试一试清单 9 显示了以未修改形式第一次运行 StringBuilder 程序的结果,然后运行 BCELTiming 程序以加入计时信息,最后运行修改后的 StringBuilder 程序。可以看到 StringBuilder 在修改后是如何开始报告执行时间的,以及时间为何比构建的字符串长度增加更快,这是由于字符串构建代码的效率不高所致。 清单 9. 运行这个程序 dennis$ java StringBuilder 1000 2000 4000 8000 16000Constructed string of length 1000Constructed string of length 2000Constructed string of length 4000Constructed string of length 8000Constructed string of length 16000dennis$ java -cp bcel.jar:. BCELTiming StringBuilder.class buildStringdennis$ java StringBuilder 1000 2000 4000 8000 16000Call to method buildString$impl took 20 ms.Constructed string of length 1000Call to method buildString$impl took 79 ms.Constructed string of length 2000Call to method buildString$impl took 250 ms.Constructed string of length 4000Call to method buildString$impl took 879 ms.Constructed string of length 8000Call to method buildString$impl took 3875 ms.Constructed string of length 16000回页首包装 BCELBCEL 有比我在本文中所介绍的基本类操作支持更多的功能。它还包括完整的验证器实现以保证二进制类对于 JVM 规范是有效的(参见 org.apache.bcel.verifier.VerifierFactory ),一个生成很好地分帧并链接的 JVM 级二进制类视图的反汇编程序,甚至一个 BCEL 程序生成器,它输出源代码以让 BCEL 程序编译所提供的类。( org.apache.bcel.util.BCELifier 类没有包括在 Javadocs 中,所以其用法要看源代码。这个功能很吸引人,但是输出对大多数开发人员来说可能人过于隐晦了)。 我自己使用 BCEL 时,发现 HTML 反汇编程序特别有用。要想试用它,只要执行 BCEL JAR 中的 org.apache.bcel.util.Class2HTML 类,用要反汇编的类文件的路径作为命令行参数。它会在当前目录中生成 HTML 文件。例如,下面我将反汇编在计时例子中使用的 StringBuilder 类: dennis$ java -cp bcel.jar org.apache.bcel.util.Class2HTML StringBuilder.classProcessing StringBuilder.class.Done.图 1 是反汇编程序生成的分帧输出的屏幕快照。在这个快照中,右上角的大帧显示了添加到 StringBuilder 类中的计时包装器方法的分解。在下载文件中有完整的 HTML 输出如果要实际观看它,只需在浏览器窗口中打开 StringBuilder.html 文件。 图 1. 反汇编
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025年鹰潭辅警协警招聘考试真题含答案详解(完整版)
- 2025年随州辅警招聘考试真题附答案详解(轻巧夺冠)
- 2025年红河州辅警招聘考试真题及答案详解(全优)
- 2025年酒泉辅警协警招聘考试备考题库及答案详解(基础+提升)
- 2025年滨州辅警协警招聘考试备考题库含答案详解(达标题)
- 2025汽车销售提成合同
- 2025赠与合同无偿赠与合同的式样
- 2025年阿勒泰辅警招聘考试题库及完整答案详解1套
- 2025年韶关辅警协警招聘考试备考题库及一套参考答案详解
- 2025年石柱县辅警招聘考试题库附答案详解(能力提升)
- 小小牙医活动方案流程
- 低压断路器课件
- 中职学考《哲学与人生》考试复习题库(含答案)
- 人教版一年级上册道德与法治教案全册
- 老年人服务礼仪与沟通技巧
- 《社会性动物》读书分享
- 《试验数据统计分析》课件
- 林区施工防火安全施工方案
- 农业水利工程专业大学生职业生涯规划书
- 新概念第二册Lesson-41课件
- 云南省红河绿春X波段天气雷达建设项目环评报告
评论
0/150
提交评论