从本系列前面的文章中,您了解到反射的性能比直接访问要慢许多倍,并了解了用 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", "<init>", 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, "<init>", 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直接推出的两个测试工具的文章。
参考资料
关于作者
分享到:
相关推荐
代码范例列表 第1章 ... useArray2.java 用反射机制使用数组示例2 第10章 示例描述:本章学习泛型。 demoBounds.java 演示有界类型 demoForceChange.java 演示强制类型转换 demoGeneric.java ...
Java 1.8 API离线手册是一份详尽的编程参考文档,专为Java开发者设计,特别是对于使用Java 8版本的开发人员来说,它是一个不可或缺的工具。这份手册包含了Java 8的所有核心库、类、接口、枚举以及异常等的详细说明,...
java.sql 提供使用 JavaTM 编程语言访问并处理存储在数据源(通常是一个关系数据库)中的数据的 API。 java.text 提供以与自然语言无关的方式来处理文本、日期、数字和消息的类和接口。 java.text.spi java.text ...
此外,Java 8还增强了反射和类型注解的处理,例如Type Annotations,允许在运行时检查类型信息,这对于验证和校验代码、库的实现以及动态代码生成非常有用。默认方法也是Java 8的一个亮点,允许在接口中添加非抽象...
9. **泛型**: 泛型是Java 5引入的新特性,允许在类、接口和方法中使用类型参数,提高了代码的类型安全性和重用性。 10. **注解**: `java.lang.annotation`包提供了注解的定义和处理机制。注解是元数据的一种形式,...
6. **JSP (JavaServer Pages)**:一种动态网页技术,将Java代码嵌入HTML中,用于生成动态内容。JSP页面在服务器端被转换成Servlet执行。 7. **Servlet**:Servlet是Java编写的小型服务器端程序,用于扩展服务器功能...
8. **反射机制**:`java.lang.reflect`包提供了反射API,可以在运行时动态获取类的信息,并能实例化对象、调用方法、访问字段等。 9. **注解(Annotation)**:Java 6引入了注解,这是一种元数据,可以添加到代码的...
java.sql 提供使用 JavaTM 编程语言访问并处理存储在数据源(通常是一个关系数据库)中的数据的 API。 java.text 提供以与自然语言无关的方式来处理文本、日期、数字和消息的类和接口。 java.text.spi java.text ...
而学习如何使用Javadoc生成API文档,是提高代码可维护性的重要步骤。对于高级主题,如反射、动态代理和 Annotation,这些通常在进阶实验中会有所涉及。 最后,单元测试和持续集成也是现代软件开发不可或缺的部分。...
9. **Java反射**:Java反射机制允许在运行时检查和操作类、接口、字段和方法,可以动态调用方法,创建对象等,提供了强大的编程灵活性。 10. **Java枚举**:枚举类型是Java 5引入的,用于定义一组固定的常量,增强...
2. **集合框架**:`java.util`包下的集合框架是Java编程的重要部分,包括List(如ArrayList和LinkedList)、Set(如HashSet和TreeSet)和Map(如HashMap和TreeMap)。此外,`java.util.concurrent`包中的并发集合类...
16. **注解(Annotation)**:元数据,提供给编译器或运行时系统使用,用于自动生成代码、验证代码、配置应用等。 17. **多线程**:Java内置对多线程的支持,可以方便地创建和管理并发执行的任务,提高程序效率。 ...
Java API 是Java应用程序接口的缩写,它是Java语言的核心组成部分,提供了一系列预先定义好的类和接口,使得开发者能够方便地进行程序开发。...在实际编程中,正确选择和使用API可以显著提高代码的效率和可维护性。
21. **反射(Reflection)**:Java反射机制允许在运行时动态访问类的信息,如类名、方法、属性等。 22. **集合框架(Collections Framework)**:提供了一组接口和类,用于高效管理和操作数据集合,如List、Set和...
java.sql 提供使用 JavaTM 编程语言访问并处理存储在数据源(通常是一个关系数据库)中的数据的 API。 java.text 提供以与自然语言无关的方式来处理文本、日期、数字和消息的类和接口。 java.text.spi java.text ...
在Java编程领域,JDK(Java Development Kit)是不可或缺的基础工具,它是Java开发环境的核心组成部分,提供了编译、运行Java程序所需的所有工具和库。本笔记将深入探讨JDK的主要功能、组件以及如何使用它们进行有效...
【博客代码大全(一).doc】这篇文档显然是一份关于编程和网页设计的教程,主要关注Java编程语言以及一些网页特效的实现。首先,我们来深入探讨Java。 Java是一种广泛使用的面向对象的编程语言,由Sun Microsystems...
《Linux多线程服务端编程:使用muduo C++网络库》主要讲述采用现代C++在x86-64 Linux上编写多线程TCP网络服务程序的主流常规技术,重点讲解一种适应性较强的多线程服务器的编程模型,即one loop per thread。...
Java工具类是Java编程中非常重要的组成部分,它们提供了一系列实用的方法,可以极大地简化开发者的工作,提高代码的可重用性和效率。在这个名为"java工具类.zip"的压缩包中,我们很可能找到了一些预定义的、方便使用...
Java API JDK 1.8中文版是Java开发者的重要参考资料,它包含了Java开发工具集的完整中文文档,方便了中文用户理解和使用Java编程语言。本文将深入解析Java API JDK 1.8中的关键知识点,帮助开发者更好地掌握这个版本...