- 浏览: 415059 次
- 性别:
- 来自: 广州
文章分类
最新评论
-
liyuanhoa_:
...
struts2.0中struts.xml配置文件详解 -
chenmingde:
...
Velocity应用(一) -
weizhikai_ai:
第二十六,当一个线程进入一个对象的一个synchronized ...
Java常见面试题(含答案) -
Aurora_lr:
...
Spring宠物商店学习笔记(一) - -
zs911zs:
all copy from http://www.iteye ...
Mule入门文档
Apache BCEL 让您深入 JVM 汇编语言进行类操作的细节
文档选项
打印本页
将此页作为电子邮件发送
级别: 初级
Dennis M. Sosnoski (dms@sosnoski.com), 总裁, Sosnoski Software Solutions, Inc.
2004 年 4 月 01 日
Apache Byte Code Engineering Library (BCEL)可以深入 Java 类的字节码。可以用它转换现有的类表示或者构建新的类,因为 BCEL 在单独的 JVM 指令级别上进行操作,所以可以让您对代码有最强大的控制。不过,这种能力的代价是复杂性。在本文中,Java 顾问 Dennis Sosnoski 介绍了 BCEL 的基本内容,并引导读者完成一个示例 BCEL 应用程序,这样您就可以自己决定是否值得以这种复杂性来换取这种能力。
在本系列的最后三篇文章中,我展示了如何用 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(argv[i]));
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(argv[i]));
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 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());
清单 4 给出了转换方法的下一段代码。这里的第一部分计算方法调用参数在堆栈上占用的空间。之所以需要这段代码,是因为为了在调用包装方法之前在堆栈帧上存储开始时间,我需要知道局部变量可以使用什么偏移值(注意,我可以用 BCEL 的局部变量处理得到同样的效果,但是在本文中我选择使用显式的方式)。这段代码的第二部分生成对 java.lang.System.currentTimeMillis() 的调用,以得到开始时间,并将它保存到堆栈帧中计算出的局部变量偏移处。
您可能会奇怪为什么在开始参数大小计算时要检查方法是否是静态的,如果是静态的,将堆栈帧槽初始化为零(不是静态正好相反)。这种方式与 Java 如何处理方法调用有关。对于非静态的方法,每次调用的第一个(隐藏的)参数是目标对象的 this 引用,在计算堆栈帧中完整参数集大小时我要考虑到这点。
清单 4. 设置包装的调用
// 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",
"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 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();
}
Type result = methgen.getReturnType();
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));
}
现在开始包装。清单 6 生成实际计算开始时间后经过的毫秒数,并作为编排好格式的消息打印出来的代码。这一部分看上去很复杂,但是大多数操作实际上只是写出输出消息的各个部分。它确实展示了几种我在前面的代码中没有使用的操作类型,包括字段访问(到 java.lang.System.out )和几种不同的指令类型。如果将 JVM 想象为基于堆栈的处理器,则其中大多数是容易理解的,因此我在这里就不再详细说明了。
清单 6. 计算并打印所使用的时间
// 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));
生成了计时消息代码后,留给清单 7 的就是保存包装的方法的调用结果值(如果有的话),然后结束构建的包装器方法。最后这部分涉及几个步骤。调用 stripAttributes(true) 只是告诉 BCEL 不对构建的方法生成调试信息,而 setMaxStack() 和 setMaxLocals() 调用计算并设置方法的堆栈使用信息。完成了这一步后,就可以实际生成方法的最终版本,并将它加入到类中。
清单 7. 完成包装器
// 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();
完整的代码
清单 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 += types[i].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 = types[i];
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 && 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 显示了以未修改形式第一次运行 StringBuilder 程序的结果,然后运行 BCELTiming 程序以加入计时信息,最后运行修改后的 StringBuilder 程序。可以看到 StringBuilder 在修改后是如何开始报告执行时间的,以及时间为何比构建的字符串长度增加更快,这是由于字符串构建代码的效率不高所致。
清单 9. 运行这个程序
[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Constructed string of length 1000
Constructed string of length 2000
Constructed string of length 4000
Constructed string of length 8000
Constructed string of length 16000
[dennis]$ java -cp bcel.jar:. BCELTiming StringBuilder.class buildString
[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Call to method buildString$impl took 20 ms.
Constructed string of length 1000
Call to method buildString$impl took 79 ms.
Constructed string of length 2000
Call to method buildString$impl took 250 ms.
Constructed string of length 4000
Call to method buildString$impl took 879 ms.
Constructed string of length 8000
Call to method buildString$impl took 3875 ms.
Constructed string of length 16000
回页首
包装 BCEL
BCEL 有比我在本文中所介绍的基本类操作支持更多的功能。它还包括完整的验证器实现以保证二进制类对于 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.class
Processing StringBuilder.class...Done.
图 1 是反汇编程序生成的分帧输出的屏幕快照。在这个快照中,右上角的大帧显示了添加到 StringBuilder 类中的计时包装器方法的分解。在下载文件中有完整的 HTML 输出——如果要实际观看它,只需在浏览器窗口中打开 StringBuilder.html 文件。
图 1. 反汇编 StringBuilder
当前,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,本系列的下一篇文章将深入比我们目前已经介绍的用途更大的类操作应用程序。在 第 2 部分,我展示了方法调用反射比直接调用慢得多。在第 8 部分中,我将显示如何使用 Javassist 和 BCEL,以便用运行时动态生成的代码替换反射调用,从而极大地提高性能。下个月请回来看另一篇 Java 编程的动态性以了解详情。
参考资料
您可以参阅本文在 developerWorks 全球站点上的 英文原文.
下载 示例代码,包括已修改类的反汇编程序输出。
在 Apache 项目页面上获得开放源代码 Byte Code Engineering Library 的所有细节。
从 Peter Haggar 的“ Java bytecode: Understanding bytecode makes you a better programmer”( developerWorks,2001 年 7 月)中了解更多关于 Java 字节码设计的内容。
请参阅 Bill Venners 的“ nside the Java Virtual Machine”(Artima Software, Inc., 2004)中关于 JVM 体系结构和指令集的很棒的参考。可以在购买之前 在线查看示例章节。
可以 在线购买或者查看官方 Java 虚拟机规范以了解 JVM 操作所有方面的权威描述。
AspectJ 用面向方面的功能扩展了 Java 语言,用 BCEL 将代码加入到由编译器生成的类中。在 Eclipse 项目页面上了解所有有关内容。
有关其他使用 BCEL 的一些项目,可查看关于 XSL 样式表的 Apache Xalan XSLTC、在测试中监视代码覆盖率的 Hansel JUnit extension和作者自己的用于快速 XML 数据绑定的 JiBX framework。
要了解更多面向方面编程的内容吗?可以从“ 使用面向 Aspect 的编程改进模块性 ” (developerWorks, 2002 年 1 月)中找到关于使用 AspectJ 语言的概述。
开放源代码 Jikes 项目提供了一个非常快速和高度兼容的 Java 编程语言的编译器。用它以老的方式生成您的字节码 —— 从 Java 源代码生成。
在 developerWorks Java 技术专区可以找到关于 Java 编程各方面的数百篇文章。
访问 Developer Bookstore,获得技术书籍的完整列表,其中包括数百本 Java 相关主题的书籍。
关于作者
Dennis Sosnoski( dms@sosnoski.com)是西雅图地区的 Java 咨询公司 Sosnoski Software Solutions, Inc.的创始人和首席顾问。他有 30 多年的专业软件开发经验,最近几年致力于服务器端的 Java 技术,包括 servlet、Enterprise JavaBeans 和 XML。他经常在全国性的会议上就 Java 中的 XML 和 J2EE 技术发表言论。
文档选项
打印本页
将此页作为电子邮件发送
级别: 初级
Dennis M. Sosnoski (dms@sosnoski.com), 总裁, Sosnoski Software Solutions, Inc.
2004 年 4 月 01 日
Apache Byte Code Engineering Library (BCEL)可以深入 Java 类的字节码。可以用它转换现有的类表示或者构建新的类,因为 BCEL 在单独的 JVM 指令级别上进行操作,所以可以让您对代码有最强大的控制。不过,这种能力的代价是复杂性。在本文中,Java 顾问 Dennis Sosnoski 介绍了 BCEL 的基本内容,并引导读者完成一个示例 BCEL 应用程序,这样您就可以自己决定是否值得以这种复杂性来换取这种能力。
在本系列的最后三篇文章中,我展示了如何用 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(argv[i]));
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(argv[i]));
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 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());
清单 4 给出了转换方法的下一段代码。这里的第一部分计算方法调用参数在堆栈上占用的空间。之所以需要这段代码,是因为为了在调用包装方法之前在堆栈帧上存储开始时间,我需要知道局部变量可以使用什么偏移值(注意,我可以用 BCEL 的局部变量处理得到同样的效果,但是在本文中我选择使用显式的方式)。这段代码的第二部分生成对 java.lang.System.currentTimeMillis() 的调用,以得到开始时间,并将它保存到堆栈帧中计算出的局部变量偏移处。
您可能会奇怪为什么在开始参数大小计算时要检查方法是否是静态的,如果是静态的,将堆栈帧槽初始化为零(不是静态正好相反)。这种方式与 Java 如何处理方法调用有关。对于非静态的方法,每次调用的第一个(隐藏的)参数是目标对象的 this 引用,在计算堆栈帧中完整参数集大小时我要考虑到这点。
清单 4. 设置包装的调用
// 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",
"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 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();
}
Type result = methgen.getReturnType();
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));
}
现在开始包装。清单 6 生成实际计算开始时间后经过的毫秒数,并作为编排好格式的消息打印出来的代码。这一部分看上去很复杂,但是大多数操作实际上只是写出输出消息的各个部分。它确实展示了几种我在前面的代码中没有使用的操作类型,包括字段访问(到 java.lang.System.out )和几种不同的指令类型。如果将 JVM 想象为基于堆栈的处理器,则其中大多数是容易理解的,因此我在这里就不再详细说明了。
清单 6. 计算并打印所使用的时间
// 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));
生成了计时消息代码后,留给清单 7 的就是保存包装的方法的调用结果值(如果有的话),然后结束构建的包装器方法。最后这部分涉及几个步骤。调用 stripAttributes(true) 只是告诉 BCEL 不对构建的方法生成调试信息,而 setMaxStack() 和 setMaxLocals() 调用计算并设置方法的堆栈使用信息。完成了这一步后,就可以实际生成方法的最终版本,并将它加入到类中。
清单 7. 完成包装器
// 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();
完整的代码
清单 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 += types[i].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 = types[i];
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 && 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 显示了以未修改形式第一次运行 StringBuilder 程序的结果,然后运行 BCELTiming 程序以加入计时信息,最后运行修改后的 StringBuilder 程序。可以看到 StringBuilder 在修改后是如何开始报告执行时间的,以及时间为何比构建的字符串长度增加更快,这是由于字符串构建代码的效率不高所致。
清单 9. 运行这个程序
[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Constructed string of length 1000
Constructed string of length 2000
Constructed string of length 4000
Constructed string of length 8000
Constructed string of length 16000
[dennis]$ java -cp bcel.jar:. BCELTiming StringBuilder.class buildString
[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Call to method buildString$impl took 20 ms.
Constructed string of length 1000
Call to method buildString$impl took 79 ms.
Constructed string of length 2000
Call to method buildString$impl took 250 ms.
Constructed string of length 4000
Call to method buildString$impl took 879 ms.
Constructed string of length 8000
Call to method buildString$impl took 3875 ms.
Constructed string of length 16000
回页首
包装 BCEL
BCEL 有比我在本文中所介绍的基本类操作支持更多的功能。它还包括完整的验证器实现以保证二进制类对于 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.class
Processing StringBuilder.class...Done.
图 1 是反汇编程序生成的分帧输出的屏幕快照。在这个快照中,右上角的大帧显示了添加到 StringBuilder 类中的计时包装器方法的分解。在下载文件中有完整的 HTML 输出——如果要实际观看它,只需在浏览器窗口中打开 StringBuilder.html 文件。
图 1. 反汇编 StringBuilder
当前,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,本系列的下一篇文章将深入比我们目前已经介绍的用途更大的类操作应用程序。在 第 2 部分,我展示了方法调用反射比直接调用慢得多。在第 8 部分中,我将显示如何使用 Javassist 和 BCEL,以便用运行时动态生成的代码替换反射调用,从而极大地提高性能。下个月请回来看另一篇 Java 编程的动态性以了解详情。
参考资料
您可以参阅本文在 developerWorks 全球站点上的 英文原文.
下载 示例代码,包括已修改类的反汇编程序输出。
在 Apache 项目页面上获得开放源代码 Byte Code Engineering Library 的所有细节。
从 Peter Haggar 的“ Java bytecode: Understanding bytecode makes you a better programmer”( developerWorks,2001 年 7 月)中了解更多关于 Java 字节码设计的内容。
请参阅 Bill Venners 的“ nside the Java Virtual Machine”(Artima Software, Inc., 2004)中关于 JVM 体系结构和指令集的很棒的参考。可以在购买之前 在线查看示例章节。
可以 在线购买或者查看官方 Java 虚拟机规范以了解 JVM 操作所有方面的权威描述。
AspectJ 用面向方面的功能扩展了 Java 语言,用 BCEL 将代码加入到由编译器生成的类中。在 Eclipse 项目页面上了解所有有关内容。
有关其他使用 BCEL 的一些项目,可查看关于 XSL 样式表的 Apache Xalan XSLTC、在测试中监视代码覆盖率的 Hansel JUnit extension和作者自己的用于快速 XML 数据绑定的 JiBX framework。
要了解更多面向方面编程的内容吗?可以从“ 使用面向 Aspect 的编程改进模块性 ” (developerWorks, 2002 年 1 月)中找到关于使用 AspectJ 语言的概述。
开放源代码 Jikes 项目提供了一个非常快速和高度兼容的 Java 编程语言的编译器。用它以老的方式生成您的字节码 —— 从 Java 源代码生成。
在 developerWorks Java 技术专区可以找到关于 Java 编程各方面的数百篇文章。
访问 Developer Bookstore,获得技术书籍的完整列表,其中包括数百本 Java 相关主题的书籍。
关于作者
Dennis Sosnoski( dms@sosnoski.com)是西雅图地区的 Java 咨询公司 Sosnoski Software Solutions, Inc.的创始人和首席顾问。他有 30 多年的专业软件开发经验,最近几年致力于服务器端的 Java 技术,包括 servlet、Enterprise JavaBeans 和 XML。他经常在全国性的会议上就 Java 中的 XML 和 J2EE 技术发表言论。
发表评论
-
java关键字native,volatile,transient
2009-02-25 15:12 13681.volatile:为了获得最佳速度,java允许线程保存共 ... -
quartz+spring
2009-02-12 14:48 3364Quartz 是一个强大的企业级 Schedule 工具,也是 ... -
Java虚拟机(JVM)参数配置说明
2009-01-08 16:02 2623在Java、J2EE大型应用中,JVM非标准参数的配置直接关系 ... -
@SuppressWarnings 详解
2009-01-08 15:27 989@SuppressWarnings J2SE 提供的最后一个 ... -
批量反编译java类文件
2009-01-08 10:30 1573所使用的软件是 DJ Java Decompiler 3.7。 ... -
LDAP定义
2008-12-23 10:20 1185LDAP是轻量目录访问协议 ... -
JMS总结和参考资料
2008-12-22 17:33 1820结束语 第 1 页(共3 页) 本教程提供了对 Ja ... -
JMS--Pub/Sub编程
2008-12-22 17:27 1108相同又不同 第 1 页(共1 页) 附录中有这两个 ... -
JMS--Pub/Sub接口
2008-12-22 17:24 1902介绍 第 1 页(共7 页) 现在让我们来查看 Pub/s ... -
JMS点对点编程
2008-12-22 17:15 1642介绍 第 1 页(共10 页) ... -
JMS点对点接口
2008-12-22 17:03 1491介绍 第 1 页(共8 页) ... -
JMS用公用接口进行客户机编程
2008-12-22 17:01 975绍 第 1 页(共11 页) 在这一节中,我们将查看 ... -
JMS公用接口
2008-12-19 18:01 1078在这一节中,我们将介绍 JMS 客户机编程的每一个重要 JMS ... -
JMS学习教程概述
2008-12-19 17:55 3128应用程序 第 1 页(共15 页) http://liy ... -
JNDI定义
2008-12-19 09:56 1060Java术语 英文全称是:Java Naming and ... -
Java 编程的动态性,第 8 部分: 用代码生成取代反射
2008-12-18 11:52 1009运行时代码生成为获得 ... -
Java 编程的动态性,第 6 部分: 利用 Javassist 进行面向方面的更改
2008-12-18 11:50 1135用 Javassist 进行字节码 ... -
Java 编程的动态性,第 5 部分: 动态转换类
2008-12-18 11:48 1054学习如何在使用 Javassist ... -
Java 编程的动态性, 第四部分: 用 Javassist 进行类转换
2008-12-18 11:47 1056用 Javassist 转换字节码中的方法 文档 ... -
Java 编程的动态性,第3部分: 应用反射
2008-12-18 11:18 838为命令行参数构建一个框架 文档选项 打印本 ...
相关推荐
在Java中,我们可以使用字节码技术来实现动态代码修改、增强或优化,这在很多场景下非常有用,比如AOP(面向切面编程)、性能监控和调试工具等。 BCEL(Byte Code Engineering Library),全称Java字节码操纵库,是...
2. **字节码生成**:BCEL提供了一套接口和类,使得开发者可以动态地生成字节码,这对于创建动态代理、AOP(面向切面编程)等场景非常有用。 3. **代码优化**:通过修改字节码,BCEL可以实现代码的优化,例如去除...
总的来说,BCEL是一个强大的工具,它为Java开发者提供了一种直接操作字节码的途径,极大地扩展了Java编程的可能性。无论是进行代码分析、动态生成还是其他高级功能,BCEL都是一个值得信赖的伙伴。
Java字节码操作库BCEL(Byte Code Engineering Library)是一个强大的工具,用于分析、修改和创建Java类文件。它是Apache软件基金会的Jakarta项目的一部分,为开发者提供了对字节码的底层控制,使得开发者能够在运行...
Java字节码操作库BCEL(Byte Code Engineering Library)是一个强大的工具,用于处理Java字节码。它允许开发者分析、创建、修改以及适应Java类文件,从而深入理解并操纵Java虚拟机(JVM)的工作原理。这个库在很多...
PMD是一个静态代码分析工具,它使用BCEL来解析Java源码并生成相应的字节码,然后对字节码进行分析,查找潜在的编程问题。通过BCEL,PMD能够高效地检查代码质量,如未使用的变量、过长的方法、空的if语句等。 五、...
BCML是一个Java字节码操纵和分析框架,它允许开发者对Java类文件进行操作和分析,包括创建、修改和动态生成。这个工具主要用于生成和操作Java虚拟机(JVM)字节码,常用于编译器、动态代理、代码分析和优化等场景。 ...
6. **字节码操纵**:Java字节码操纵库,如ASM、BCEL、CGLIB等,允许程序员在运行时动态修改类的字节码,实现AOP(面向切面编程)和代码注入等功能。 7. **安全性**:Java的字节码验证机制确保了字节码的安全性,...
BCEL是Apache软件基金会下的Jakarta项目的一部分,它允许程序员以编程方式访问和修改Java类文件的字节码。通过BCEL,开发者可以创建新的类、接口,或者在已有的类上添加、修改方法和字段,甚至可以实现复杂的代码...
Apache Commons BCEL(Byte Code Engineering Library)是Apache软件基金会的一个开源项目,主要目的是提供对Java字节码的高级操作。这个库允许开发者分析、创建、修改和编译Java类文件,为开发动态类生成和代码分析...
Java字节码实现AOP(面向切面编程)是一种在程序运行时动态插入代码的技术,它使得我们可以在不修改原有代码的情况下,增加新的功能或监控已有功能。在Java中,AOP通常通过代理模式和字节码操作来实现,如Spring AOP...
Java字节码操作库BCEL(Byte Code Engineering Library)是Apache软件基金会提供的一个开源项目,主要用于解析、生成和修改Java字节码。在深入理解BCEL之前,我们首先要明白Java字节码的概念。Java字节码是Java源...
《Ant Apache BCEL 1.6.4:Java字节码操作库的深入解析》 在Java开发领域,Ant是一个广泛使用的自动化构建工具,而Apache BCEL(Byte Code Engineering Library)则是Ant的一个重要扩展,提供了对Java字节码的生成...
5. **注解处理**:Java字节码中包含了注解信息,这些注解可能包含有用的设计意图或元数据,可用于代码自动生成、AOP(面向切面编程)等。 6. **反射信息**:Java字节码也记录了类、方法和字段的反射信息,这对于...
Apache BCEL(Byte Code Engineering Library)则是Apache Commons项目的一部分,它提供了一组高级API,用于分析、创建和修改Java字节码。通过BCEL,开发者可以深入到程序的字节码层面,进行诸如性能优化、动态代码...
总的来说,"ant-apache-bcel-1.6.2.jar.zip"为Java开发者提供了一个强大的工具,结合Ant的便利性和BCEL的字节码处理能力,使得项目构建和维护变得更加灵活和高效。理解并熟练运用这个库,能够提升开发者在字节码层面...
3. **Java代理(JProxy)与CGLIB**:除了标准的Java动态代理,还有一些第三方库如CGLIB,它们能生成字节码以创建不依赖接口的代理,扩展了动态代理的使用场景。 4. **Java模块系统(JSR 294 & JPMS)**:Java 9引入...