Java 编程的动态性,第 8 部分: 用代码生成取代反射
Dennis Sosnoski (dms@sosnoski.com) 总裁, Sosnoski Software Solutions, Inc. 2004 年 6 月
从本系列前面的文章中,您了解到反射的性能比直接访问要慢许多倍,并了解了用 Javassist 和 Apache Byte Code Engineering Library (BCEL)进行classworking。Java 顾问 Dennis Sosnoski 通过演示如何使用运行时 classworking,来用全速前进的生成代码取代反射代码,从而结束他的 Java 编程的动态性 系列。 既然您已经看到了如何使用 Javassist 和 BCEL 框架来进行 classworking (请参阅 本系列以前的一组文章), 我将展示一个实际的 classworking 应用程序。这个应用程序用运行时生成的、并立即装载到 JVM 的类来取代反射。在综合讨论的过程中,我将引用本系列的前两篇文章,以及对 Javassist 和 BCEL 的讨论,这样本文就成为了对这个很长的系列文章的一个很好的总结。
反射的性能 在 第 2 部分, 我展示了无论是对于字段访问还是方法调用,反射都比直接代码慢很多倍。这种延缓对于许多应用程序来说不算是问题,但是总是会遇到性能非常关键的情况。在这种情况下,反射可能成为真正的瓶颈。但是,用静态编译的代码取代反射可能会非常混乱,并且在有些情况下(如在这种框架中:反射访问的类或者项目是在运行时提供的,而不是作为这一编译过程的一部分提供的),如果不重新构建整个应用程序就根本不可能取代。
Classworking 使我们有机会将静态编译的代码的性能与反射的灵活性结合起来。这里的基本方法是,在运行时,以一种可以被一般性代码使用的方式,构建一个自定义的类,其中将包装对目标类的访问(以前是通过反射达到的)。将这个自定义类装载到 JVM 中后,就可以全速运行了。
设置阶段 清单 1 给出了应用程序的起点。这里定义了一个简单的 bean 类 HolderBean 和一个访问类 ReflectAccess。访问类有一个命令行参数,该参数必须是一个值为 int 的 bean 类属性的名字(value1 或者 value2)。它增加指定属性的值,然后在退出前打印出这两个属性值。
清单 1. 反射一个 bean
public class HolderBean { private int m_value1; private int m_value2;
public int getValue1() { return m_value1; } public void setValue1(int value) { m_value1 = value; }
public int getValue2() { return m_value2; } public void setValue2(int value) { m_value2 = value; } } public class ReflectAccess { public void run(String[] args) throws Exception { if (args.length == 1 && args[0].length() > 0) {
// create property name char lead = args[0].charAt(0); String pname = Character.toUpperCase(lead) + args[0].substring(1);
// look up the get and set methods Method gmeth = HolderBean.class.getDeclaredMethod ("get" + pname, new Class[0]); Method smeth = HolderBean.class.getDeclaredMethod ("set" + pname, new Class[] { int.class });
// increment value using reflection HolderBean bean = new HolderBean(); Object start = gmeth.invoke(bean, null); int incr = ((Integer)start).intValue() + 1; smeth.invoke(bean, new Object[] {new Integer(incr)});
// print the ending values System.out.println("Result values " + bean.getValue1() + ", " + bean.getValue2());
} else { System.out.println("Usage: ReflectAccess value1|value2"); } } }
下面是运行 ReflectAccess 的两个例子,用来展示结果:
[dennis]$ java -cp . ReflectAccess value1 Result values 1, 0 [dennis]$ java -cp . ReflectAccess value2 Result values 0, 1
构建 glue 类 我已经展示了反射版本的代码,现在要展示如何用生成的类来取代反射。要想让这种取代可以正确工作,会涉及到一个微妙的问题,它可追溯到本系列 第 1 部分 中对类装载的讨论。这个问题是:我想要在运行时生成一个可从访问类的静态编译的代码进行访问的类,但是因为对编译器来说生成的类不存在,因此没办法直接引用它。
那么如何将静态编译的代码链接到生成的类呢?基本的解决方案是定义可以用静态编译的代码访问的基类或者接口,然后生成的类扩展这个基类或者实现这个接口。这样静态编译的代码就可以直接调用方法,即使方法只有到了运行时才能真正实现。
在清单 2 中,我定义了一个接口 IAccess,目的是为生成的代码提供这种链接。这个接口包括三个方法。第一个方法只是设置要访问的目标对象。另外两个方法是用于访问一个 int 属性值的 get 和 set 方法的代理。
清单 2. 到 glue 类的接口
public interface IAccess { public void setTarget(Object target); public int getValue(); public void setValue(int value); }
这里的意图是让 IAccess 接口的生成实现提供调用目标类的相应 get 和 set 方法的代码。清单 3 显示了实现这个接口的一个例子,假定我希望访问 清单 1 中 HolderBean 类的 value1 属性:
清单 3. Glue 类示例实现
public class AccessValue1 implements IAccess { private HolderBean m_target;
public void setTarget(Object target) { m_target = (HolderBean)target; } public int getValue() { return m_target.getValue1(); } public void setValue(int value) { m_target.setValue1(value); } }
清单 2 接口设计为针对特定类型对象的特定属性使用。这个接口使实现代码简单了 —— 在处理字节码时这总是一个优点 —— 但是也意味着实现类是非常特定的。对于要通过这个接口访问的每一种类型的对象和属性,都需要一个单独的实现类,这限制了将这种方法作为反射的一般性替代方法。 如果选择只在反射性能真正成为瓶颈的情况下才使用这种技术,那么这种限制就不是一个问题。
用 Javassist 生成 用 Javassist 为 清单 2 IAccess 接口生成实现类很容易 —— 只需要创建一个实现了这个接口的新类、为目标对象引用添加一个成员变量、最后再添加一个无参构造函数和简单实现方法。清单 4 显示了完成这些步骤的 Javassist 代码,它构造一个方法调用,这个方法以目标类和 get/set 方法信息为参数、并返回所构造的类的二进制表示:
清单 4. Javassist glue 类构造
/** Parameter types for call with no parameters. */ private static final CtClass[] NO_ARGS = {};
/** Parameter types for call with single int value. */ private static final CtClass[] INT_ARGS = { CtClass.intType };
protected byte[] createAccess(Class tclas, Method gmeth, Method smeth, String cname) throws Exception {
// build generator for the new class String tname = tclas.getName(); ClassPool pool = ClassPool.getDefault(); CtClass clas = pool.makeClass(cname); clas.addInterface(pool.get("IAccess")); CtClass target = pool.get(tname);
// add target object field to class CtField field = new CtField(target, "m_target", clas); clas.addField(field);
// add public default constructor method to class CtConstructor cons = new CtConstructor(NO_ARGS, clas); cons.setBody(";"); clas.addConstructor(cons);
// add public setTarget method CtMethod meth = new CtMethod(CtClass.voidType, "setTarget", new CtClass[] { pool.get("java.lang.Object") }, clas); meth.setBody("m_target = (" + tclas.getName() + ")$1;"); clas.addMethod(meth);
// add public getValue method meth = new CtMethod(CtClass.intType, "getValue", NO_ARGS, clas); meth.setBody("return m_target." + gmeth.getName() + "();"); clas.addMethod(meth);
// add public setValue method meth = new CtMethod(CtClass.voidType, "setValue", INT_ARGS, clas); meth.setBody("m_target." + smeth.getName() + "($1);"); clas.addMethod(meth);
// return binary representation of completed class return clas.toBytecode(); }
我不准备详细讨论这些代码,因为如果您一直跟着学习本系列,这里的大多数操作都是所熟悉的(如果您 还没有 看过本系列,请现在阅读 第 5 部分,了解使用 Javassist 的概述)。
用 BCEL 生成 用 BCEL 生成 清单 2 IAccess 的实现类不像使用 Javassist 那样容易,但是也不是很复杂。清单 5 给出了相应的代码。 这段代码使用与清单 4 Javassist 代码相同的一组操作,但是运行时间要长一些,因为需要为 BCEL 拼出每一个字节码指令。与使用 Javassist 时一样,我将跳过实现的细节(如果有不熟悉的地方,请参阅 第 7 部分 对 BCEL 的概述)。
清单 5. BCEL glue 类构造
/** Parameter types for call with single int value. */ private static final Type[] INT_ARGS = { Type.INT };
/** Utility method for adding constructed method to class. */ private static void addMethod(MethodGen mgen, ClassGen cgen) { mgen.setMaxStack(); mgen.setMaxLocals(); InstructionList ilist = mgen.getInstructionList(); Method method = mgen.getMethod(); ilist.dispose(); cgen.addMethod(method); }
protected byte[] createAccess(Class tclas, java.lang.reflect.Method gmeth, java.lang.reflect.Method smeth, String cname) {
// build generators for the new class String tname = tclas.getName(); ClassGen cgen = new ClassGen(cname, "java.lang.Object", cname + ".java", Constants.ACC_PUBLIC, new String[] { "IAccess" }); InstructionFactory ifact = new InstructionFactory(cgen); ConstantPoolGen pgen = cgen.getConstantPool();
//. add target object field to class FieldGen fgen = new FieldGen(Constants.ACC_PRIVATE, new ObjectType(tname), "m_target", pgen); cgen.addField(fgen.getField()); int findex = pgen.addFieldref(cname, "m_target", Utility.getSignature(tname));
// create instruction list for default constructor InstructionList ilist = new InstructionList(); ilist.append(InstructionConstants.ALOAD_0); ilist.append(ifact.createInvoke("java.lang.Object", "", Type.VOID, Type.NO_ARGS, Constants.INVOKESPECIAL)); ilist.append(InstructionFactory.createReturn(Type.VOID));
// add public default constructor method to class MethodGen mgen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID, Type.NO_ARGS, null, "", cname, ilist, pgen); addMethod(mgen, cgen);
// create instruction list for setTarget method ilist = new InstructionList(); ilist.append(InstructionConstants.ALOAD_0); ilist.append(InstructionConstants.ALOAD_1); ilist.append(new CHECKCAST(pgen.addClass(tname))); ilist.append(new PUTFIELD(findex)); ilist.append(InstructionConstants.RETURN);
// add public setTarget method mgen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID, new Type[] { Type.OBJECT }, null, "setTarget", cname, ilist, pgen); addMethod(mgen, cgen);
// create instruction list for getValue method ilist = new InstructionList(); ilist.append(InstructionConstants.ALOAD_0); ilist.append(new GETFIELD(findex)); ilist.append(ifact.createInvoke(tname, gmeth.getName(), Type.INT, Type.NO_ARGS, Constants.INVOKEVIRTUAL)); ilist.append(InstructionConstants.IRETURN);
// add public getValue method mgen = new MethodGen(Constants.ACC_PUBLIC, Type.INT, Type.NO_ARGS, null, "getValue", cname, ilist, pgen); addMethod(mgen, cgen);
// create instruction list for setValue method ilist = new InstructionList(); ilist.append(InstructionConstants.ALOAD_0); ilist.append(new GETFIELD(findex)); ilist.append(InstructionConstants.ILOAD_1); ilist.append(ifact.createInvoke(tname, smeth.getName(), Type.VOID, INT_ARGS, Constants.INVOKEVIRTUAL)); ilist.append(InstructionConstants.RETURN);
// add public setValue method mgen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID, INT_ARGS, null, "setValue", cname, ilist, pgen); addMethod(mgen, cgen);
// return bytecode of completed class return cgen.getJavaClass().getBytes(); }
性能检查 已经介绍了 Javassist 和 BCEL 版本的方法构造,现在可以试用它们以了解它们工作的情况。在运行时生成代码的根本理由是用一些更快的的东西取代反射,所以最好加入性能比较以了解在这方面的改进。为了更加有趣,我还将比较用两种框架构造 glue 类所用的时间。
清单 6 显示用于检查性能的测试代码的主要部分。runReflection() 方法运行测试的反射部分,runAccess() 运行直接访问部分,run() 控制整个进程(包括打印时间结果)。runReflection() 和 runAccess() 都取要执行的次数作为参数,这个参数是以命令行的形式传递的(使用的代码没有在清单中显示,但是包括在下载中)。DirectLoader 类(在清单 6 的结尾)只提供了装载生成的类的一种容易的方式。
清单 6. 性能测试代码
/** Run timed loop using reflection for access to value. */ private int runReflection(int num, Method gmeth, Method smeth, Object obj) { int value = 0; try { Object[] gargs = new Object[0]; Object[] sargs = new Object[1]; for (int i = 0; i < num; i++) {
// messy usage of Integer values required in loop Object result = gmeth.invoke(obj, gargs); value = ((Integer)result).intValue() + 1; sargs[0] = new Integer(value); smeth.invoke(obj, sargs);
} } catch (Exception ex) { ex.printStackTrace(System.err); System.exit(1); } return value; }
/** Run timed loop using generated class for access to value. */ private int runAccess(int num, IAccess access, Object obj) { access.setTarget(obj); int value = 0; for (int i = 0; i < num; i++) { value = access.getValue() + 1; access.setValue(value); } return value; }
public void run(String name, int count) throws Exception {
// get instance and access methods HolderBean bean = new HolderBean(); String pname = name; char lead = pname.charAt(0); pname = Character.toUpperCase(lead) + pname.substring(1); Method gmeth = null; Method smeth = null; try { gmeth = HolderBean.class.getDeclaredMethod("get" + pname, new Class[0]); smeth = HolderBean.class.getDeclaredMethod("set" + pname, new Class[] { int.class }); } catch (Exception ex) { System.err.println("No methods found for property " + pname); ex.printStackTrace(System.err); return; }
// create the access class as a byte array long base = System.currentTimeMillis(); String cname = "IAccess$impl_HolderBean_" + gmeth.getName() + "_" + smeth.getName(); byte[] bytes = createAccess(HolderBean.class, gmeth, smeth, cname);
// load and construct an instance of the class Class clas = s_classLoader.load(cname, bytes); IAccess access = null; try { access = (IAccess)clas.newInstance(); } catch (IllegalAccessException ex) { ex.printStackTrace(System.err); System.exit(1); } catch (InstantiationException ex) { ex.printStackTrace(System.err); System.exit(1); } System.out.println("Generate and load time of " + (System.currentTimeMillis()-base) + " ms.");
// run the timing comparison long start = System.currentTimeMillis(); int result = runReflection(count, gmeth, smeth, bean); long time = System.currentTimeMillis() - start; System.out.println("Reflection took " + time + " ms. with result " + result + " (" + bean.getValue1() + ", " + bean.getValue2() + ")"); bean.setValue1(0); bean.setValue2(0); start = System.currentTimeMillis(); result = runAccess(count, access, bean); time = System.currentTimeMillis() - start; System.out.println("Generated took " + time + " ms. with result " + result + " (" + bean.getValue1() + ", " + bean.getValue2() + ")"); }
/** Simple-minded loader for constructed classes. */ protected static class DirectLoader extends SecureClassLoader { protected DirectLoader() { super(TimeCalls.class.getClassLoader()); }
protected Class load(String name, byte[] data) { return super.defineClass(name, data, 0, data.length); } }
为了进行简单的计时测试,我调用 run() 方法两次,对于 清单 1 HolderBean 类中的每个属性调用一次。运行两次测试对于测试的公正性是很重要的 —— 第一次运行代码要装载所有必要的类,这对于 Javassist 和 BCEL 类生成过程都会增加大量开销。不过,在第二次运行时不需要这种开销,这样就能更好地估计在实际的系统中使用时,类生成需要多长的时间。 下面是一个执行测试时生成的示例输出:
[dennis]$$ java -cp .:bcel.jar BCELCalls 2000 Generate and load time of 409 ms. Reflection took 61 ms. with result 2000 (2000, 0) Generated took 2 ms. with result 2000 (2000, 0) Generate and load time of 1 ms. Reflection took 13 ms. with result 2000 (0, 2000) Generated took 2 ms. with result 2000 (0, 2000)
图 1 显示了用从 2k 到 512k 次循环进行调用时计时测试的结果(在运行 Mandrake Linux 9.1 的 Athlon 2200+ XP 系统上运行测试,使用 Sun 1.4.2 JVM )。这里,我在每次测试运行中加入了第二个属性的反射时间和生成的代码的时间(这样首先是使用 Javassist 代码生成的两个时间,然后是使用 BCEL 代码生成时的同样两个时间)。不管是用 Javassist 还是 BCEL 生成 glue 类,执行时间大致是相同的,这也是我预计的结果 —— 但是确认一下总是好的!
图 1. 反射速度与生成的代码的速度(时间单位为毫秒)
从图 1 中可以看出,不管在什么情况下,生成的代码执行都比反射要快得多。生成的代码的速度优势随着循环次数的增加而增加,在 2k 次循环时大约为 5:1,在 512K 次循环时增加到大约 24:1。对于 Javassist,构造并装载第一个 glue 类需要大约 320 毫秒(ms),而对于 BCEL 则为 370 ms,而构造第二个 glue 类对于 Javassist 只用 4 ms,对于 BCEL 只用 2 ms(由于时钟分辨率只有 1ms,因此这些时间是非常粗略的)。如果将这些时间结合到一起,将会看到即使对于 2k 次循环,生成一个类也比使用反射有更好的整体性能(总执行时间为约 4 ms 到 6 ms,而使用反射时大约为 14 ms)。
此外,实际情况比这个图中所表明的更有利于生成的代码。在循环减少至 25 次循环时,反射代码的执行仍然要用 6 ms 到 7 ms,而生成的代码运行得太快以致不能记录。针对相对较少的循环次数,反射所花的时间反映出,当达到一个阈值时在 JVM 中进行了某种优化,如果我将循环次数降低到少于 20,那么反射代码也会快得无法记录。
加速上路 现在已经看到了运行时 classworking 可以为应用程序带来什么样的性能。下次面临难处理的性能优化问题时要记住它 —— 它可能就是避免大的重新设计的关键所在。不过,Classworking 不仅有性能上的有好处,它还是一种使应用程序适合运行时要求的灵活方式。即使没有理由在代码中使用它,我也认为它是使编程变得有趣的一种 Java 功能。
对一个 classworking 的真实世界应用程序的探讨结束了“Java 编程的动态性”这一系列。但是不要失望 —— 当我展示一些为操纵 Java 字节码而构建的工具时,您很快就有机会在 developerWorks 中了解一些其他的 classworking 应用程序了。首先将是一篇关于 Mother Goose 直接推出的两个测试工具的文章。 |
阅读(525) | 评论(0) | 转发(0) |