Java 编程的动态性,第 8 部分: 用代码生成取代反射--转载
既然您已经看到了如何使用 Javassist 和 BCEL 框架来进行 classworking (请参阅?), 我将展示一个实际的 classworking 应用程序。这个应用程序用运行时生成的、并立即装载到 JVM 的类来取代反射。在综合讨论的过程中,我将引用本系列的前两篇文章,以及对 Javassist 和 BCEL 的讨论,这样本文就成为了对这个很长的系列文章的一个很好的总结。 在?, 我展示了无论是对于字段访问还是方法调用,反射都比直接代码慢很多倍。这种延缓对于许多应用程序来说不算是问题,但是总是会遇到性能非常关键的情况。在这种情况下,反射可能成为真正的瓶颈。但是,用静态编译的代码取代反射可能会非常混乱,并且在有些情况下(如在这种框架中:反射访问的类或者项目是在运行时提供的,而不是作为这一编译过程的一部分提供的),如果不重新构建整个应用程序就根本不可能取代。 Classworking 使我们有机会将静态编译的代码的性能与反射的灵活性结合起来。这里的基本方法是,在运行时,以一种可以被一般性代码使用的方式,构建一个自定义的类,其中将包装对目标类的访问(以前是通过反射达到的)。将这个自定义类装载到 JVM 中后,就可以全速运行了。 清单 1 给出了应用程序的起点。这里定义了一个简单的 bean 类?
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;
}
}
} 下面是运行? 我已经展示了反射版本的代码,现在要展示如何用生成的类来取代反射。要想让这种取代可以正确工作,会涉及到一个微妙的问题,它可追溯到本系列?中对类装载的讨论。这个问题是:我想要在运行时生成一个可从访问类的静态编译的代码进行访问的类,但是因为对编译器来说生成的类不存在,因此没办法直接引用它。 那么如何将静态编译的代码链接到生成的类呢?基本的解决方案是定义可以用静态编译的代码访问的基类或者接口,然后生成的类扩展这个基类或者实现这个接口。这样静态编译的代码就可以直接调用方法,即使方法只有到了运行时才能真正实现。 在清单 2 中,我定义了一个接口? 这里的意图是让?
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);
}
} ?接口设计为针对特定类型对象的特定属性使用。这个接口使实现代码简单了 —— 在处理字节码时这总是一个优点 —— 但是也意味着实现类是非常特定的。对于要通过这个接口访问的每一种类型的对象和属性,都需要一个单独的实现类,这限制了将这种方法作为反射的一般性替代方法。 如果选择只在反射性能真正成为瓶颈的情况下才使用这种技术,那么这种限制就不是一个问题。 用 Javassist 为?
// 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();
} 我不准备详细讨论这些代码,因为如果您一直跟着学习本系列,这里的大多数操作都是所熟悉的(如果您?还没有?看过本系列,请现在阅读?,了解使用 Javassist 的概述)。 用 BCEL 生成?
// 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),pgen);
cgen.addField(fgen.getField());
int findex = pgen.addFieldref(cname,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,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,new Type[] { Type.OBJECT },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,Constants.INVOKEVIRTUAL));
ilist.append(InstructionConstants.IRETURN);
// add public getValue method
mgen = new MethodGen(Constants.ACC_PUBLIC,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(),Constants.INVOKEVIRTUAL));
ilist.append(InstructionConstants.RETURN);
// add public setValue method
mgen = new MethodGen(Constants.ACC_PUBLIC,cgen);
// return bytecode of completed class
return cgen.getJavaClass().getBytes();
} 已经介绍了 Javassist 和 BCEL 版本的方法构造,现在可以试用它们以了解它们工作的情况。在运行时生成代码的根本理由是用一些更快的的东西取代反射,所以最好加入性能比较以了解在这方面的改进。为了更加有趣,我还将比较用两种框架构造 glue 类所用的时间。 清单 6 显示用于检查性能的测试代码的主要部分。?
// 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;
}
}
} 为了进行简单的计时测试,我调用? 图 1 显示了用从 2k 到 512k 次循环进行调用时计时测试的结果(在运行 Mandrake Linux 9.1 的 Athlon 2200+ XP 系统上运行测试,使用 Sun 1.4.2 JVM )。这里,我在每次测试运行中加入了第二个属性的反射时间和生成的代码的时间(这样首先是使用 Javassist 代码生成的两个时间,然后是使用 BCEL 代码生成时的同样两个时间)。不管是用 Javassist 还是 BCEL 生成 glue 类,执行时间大致是相同的,这也是我预计的结果 —— 但是确认一下总是好的! 从图 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直接推出的两个测试工具的文章。 原文:http://www.ibm.com/developerworks/cn/java/j-dyn0610/index.html (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |