代码生成器能否代替码农

    技术2024-04-15  87

    既然您已经了解了如何使用Javassist和BCEL框架进行类工作(请参阅本系列以前的文章的清单 ),我将向您展示一个实用的类工作应用程序。 该应用程序正在运行时生成并立即加载到JVM中的类取代了对反射的使用。 在将其组合在一起的过程中,我将参考该系列的前两篇文章以及Javassist和BCEL的内容,因此对于原来的较长系列而言,它是一个很好的总结。文章。

    对绩效的思考

    在第二部分中 ,我展示了对于字段访问和方法调用,反射比直接代码要慢许多倍。 对于许多应用程序而言,这种迟缓不是问题,但是在某些情况下,性能至关重要。 在这些情况下,反射可能代表真正的瓶颈。 但是,用静态编译的代码替换反射可能会很混乱,并且在某些情况下(如在框架中,反射时访问的类或项目是在运行时提供的,而不是作为同一构建过程的一部分提供的)即使不进行重组也无法实现。整个应用程序。

    类工作为您提供了一种将静态编译代码的性能与反射灵活性相结合的替代方法。 此处的基本方法是在运行时构造一个自定义类,该类将包装对目标类的访问(以前是通过反射实现的),使用的方式可以由通用代码使用。 将自定义类加载到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
    不要错过本系列的其余部分

    第1部分,“ 类和类加载 ”(2003年4月)

    第2部分,“ 介绍反射 ”(2003年6月)

    第3部分,“ 应用反射 ”(2003年7月)

    第4部分,“ 使用Javassist进行类转换 ”(2003年9月)

    第5部分,“ 即时转换类 ”(2004年2月)

    第6部分,“ 使用Javassist进行面向方面的更改 ”(2004年3月)

    第7部分,“ 使用BCEL进行字节码工程 ”(2004年4月)

    建立胶水课

    现在,我已经演示了代码的反射版本,我将向您展示如何用生成的类替换反射。 使这种替换正常工作涉及一个微妙的问题,可以追溯到本系列第1部分中有关类加载的讨论。 问题是我要在运行时生成一个类,该类要从访问类的静态编译代码访问,但是由于生成的类对于编译器不存在,因此无法直接引用它。

    那么如何将静态编译的代码链接到生成的类呢? 基本解决方案是定义可由静态编译的代码访问的基类或接口,然后扩展该基类或在生成的类中实现该接口。 静态编译的代码然后可以直接调用方法,即使方法要到运行时才真正实现。

    在清单2中,我定义了一个接口IAccess ,旨在为生成的代码提供此链接。 该界面包括三种方法。 第一种方法只是设置要访问的目标对象。 另外两个方法是用于访问int属性值的get和set方法的代理。

    清单2.胶水类的接口
    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接口被设计为与特定类型的对象的特定属性一起使用。 该接口使实现代码保持简单-在使用字节码时始终是一个优点-但意味着实现类非常具体。 我要通过此接口访问的每种对象和属性类型都需要有一个单独的实现类,这限制了将此方法用作反射的常规替代方法。 只要仅在反射性能确实是瓶颈的情况下有选择地应用该技术,此限制就不会成为问题。

    咨询专家:Dennis Sosnoski关于JVM和字节码的问题

    如果您对本系列文章中涉及的材料以及与Java字节码,Java二进制类格式或一般JVM问题有关的任何其他内容有任何意见或疑问,请访问由Dennis Sosnoski主持的JVM和Bytecode讨论论坛。

    用Javassist生成

    使用Javassist为清单2的 IAccess接口生成实现类很容易-我只需要创建一个实现该接口的新类,为目标对象引用添加一个成员变量,然后通过添加一个无参数的构造函数和简单的实现方法。 清单4显示了完成这些步骤的Javassist代码,结构为一个方法调用,该方法调用接收目标类和获取/设置方法信息,并返回所构造类的二进制表示形式:

    清单4. Javassist胶水类构造
    /** 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胶水类构造
    /** 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版的代码,现在可以尝试一下它们,看看它们的工作效果如何。 我在运行时生成代码的最初原因是用更快的速度替换反射,因此最好进行性能比较以了解我的成功程度。 为了使它有趣,我还将看看用每个框架构造胶水类所需的时间。

    清单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生成粘合类,执行时间都差不多,这是我希望看到的-但是进行确认总是很好的!

    图1.反射与生成的代码速度(时间以毫秒为单位)

    从图1可以看到,在每种情况下,生成的代码的执行速度都比反射执行的速度快得多。 生成的代码的速度优势随着循环数量的增加而增加,从2K循环大约以5:1开始,而到512K循环则以大约24:1开始。 对于Javassist,构造和加载第一个胶水类大约需要320毫秒(ms),而对于BCEL来说,则要花费370 ms,而对于Javassist,构造第二胶水类仅花费大约4 ms;对于BCEL,则要花费2 ms(尽管时钟分辨率只有1 ms ,因此这些时间非常艰难)。 如果将这些时间结合起来,您会发现,即使是在2K循环的情况下,生成类也将比使用反射提供总体上更好的性能(总执行时间约为4毫秒至6毫秒,而反射约为14毫秒)。

    实际上,这种情况似乎更偏向于生成的代码,而不是该图表似乎表明的那样。 当我尝试低至25个循环时,反射代码仍然需要6到7毫秒才能执行,而生成的代码注册起来却太快了。 相对于较少的循环计数,反射所花费的时间似乎反映了在达到阈值时JVM中正在进行的一些优化。 如果我将循环计数降低到大约20以下,则反射代码也变得太快而无法注册。

    加快您的前进速度

    现在,您已经看到了运行时类工作可以为您的应用程序提供的性能。 下一次遇到棘手的性能优化问题时,请记住这一点-它可能只是可以省去大量重新设计工作的魔术子弹。 但是,课堂工作不仅对性能有好处。 这也是一种独特的灵活方法,可根据运行时要求定制您的应用程序。 即使您从来没有理由在代码中使用它,我也认为这是Java的功能之一,它使编程变得越来越有趣。

    冒险进入类工作的实际应用程序,总结了有关Java编程动力学的系列文章。 但是请不要失望-当我介绍一些围绕Java字节码操作构建的工具时,您将很快有机会在developerWorks自助餐中试用其他一些类工作应用程序。 首先是有关Goose Goose的一对测试工具的文章。


    翻译自: https://www.ibm.com/developerworks/java/library/j-dyn0610/index.html

    相关资源:nimLUA:胶水代码生成器,使用Nim强大的宏将Nim和Lua绑定在一起-源码
    Processed: 0.015, SQL: 9