Java中操作Reflection API优化代码
副标题#e#
摘要
开拓者通过各类百般的要领来实验制止单调冗余的编程。一些编程的法则譬喻担任、多态可能设计模子可以辅佐开拓者制止发生多余的代码。不外由于软件开拓方面存在着不确定性,因此这些法则并不能消除代码维护和从头编写的需要。在许多时候维护都是不行制止的,只有不能运作的软件才是从不需要维护的。不外,这篇文章先容了你可以利用Java的Reflection API的成果来淘汰单调的代码编写,并可以利用勾当的代码发生来降服reflection的限制。
数据设置(由外部的源头获得数据而且将它装载到一个Java工具中)可以操作reflection的长处来建设一个可重用的方案。问题是很简朴的:将数据由一个文件装入到一个工具的字段中。此刻假设用作数据的方针Java类每礼拜改变一次?有一个很直接的办理要领,不外你必需不绝地维护载入的进程来反应任何的改变。在更巨大的情况下,同样的问题大概会令系统瓦解掉。对付一个处理惩罚过运用XML的大型系统的人来说,他就会碰着过这个问题。要编写一个载入的进程凡是长短常单调乏味的,由于数据源可能方针Java类的改变,你需要常常更新和从头编写代码。在这里我要先容另一个办理方案,那就是利用映射,它凡是利用更少的编码,而且可以在方针Java类产生改变后更新本身。
最初,我想先容一个利用Reflection在运行期间设置数据的方案。在开始的时候,一个动态、基于映射的措施要比一个简朴的要领更有吸引力多了。随后,我要展现出运行时Reflection的巨大性和冒险性。这篇文章将先容由运行时的Reflection到勾当的代码发生。
由简朴到巨大
我的第一个方案利用一个载入类将数据从一个文件载入到工具中。我的源代码含有对StringTokenizer工具下一节点要领的多次挪用。在修改多次后,我的编码逻辑变得很是的直接、系统化。该类结构了专用的代码。在这个初始方案中,我只需要利用3个根基的工具:
1、Strings
2、Objects
3、Arrays of objects
你可以影射类的工具来发生代码块,如下表所示:
被影射来发生代码块的工具
Field type | Code block |
String | fileIterator.nextString(); |
Object[] | Vector collector = new Vector(); while(fileIterator.hasMoreDataForArray()){ Object data = initializeObject(fileIterator)collector.add(data); } Object[] objArray = new Object[collector.size()]; collector.copyInto(objArray); |
Object | initializeObject(fileIterator); |
**************表一**************
我已经利用这个方案作了屡次编码,因此我在写代码之前我已经知道该方案和代码的布局。难点在于该类是变革的。类的名字、成份和布局在任何时候都大概产生变革,而任何的改变你都要从头编写代码。固然会产生这些变革,可是布局和下载的流程仍然是一样的;在写代码前,我仍然知道代码的布局和成份。我需要一个要领,来将脑子中的编码流程转换为一个可重用的、自动的形式。由于我是一个有效率的编程者,我很快就厌倦了编写险些一样的代码,这时我想到了映射。
数据设置凡是需要一个源到目标数据的影射。影射可以是一个图解、DTD(document type definition,文档范例界说),文件名目等。在这个例子中,映射将一个工具的类界说表明为我们要映射的流程。映射可以在运行时复制代码的成果。在需要重写代码时,我将载入的进程用映射来取代,它所需要的时间和重写是一样的。
#p#副标题#e#
载入的工程可以归纳综合为以下几步:
1、表明:一个影射抉择你在结构一个工具时需要些什么
2、请求数据:要满意结构的需要,要举办一个挪用来获得数据
3、拖:数据由源中获得。
4、推:数据被填充入一个工具的新实例
5、假如须要的话,反复步调1
你需要以下的类来满意以上的步调:
.数据类(Data classes):由ASCII文件中的数据实例化。类界说提供数据的影射。数据类必需满意以下的条件:
.它们必需包括有一个结构器来吸收全部必须的参数,以利用一个有效的状态来结构工具;
.它们必需由工具组成,这些工具是reflective进程知道如那里理惩罚的
.工具装载器(Object loader):利用reflection和数据类作为一个影射来载入数据。发生数据请求。
.载入打点器(Load manager):作为工具装载器和数据源的中介层,将对数据的请求转换为一个数据指定的挪用。这可以令工具载入器做到与数据源无关。通过它的接口和一个可载入的类工具通信。
.数据轮回接口(Data iterator interface):载入打点器和载入类工具利用这个接口理由数据源中获得数据。
一旦你建设了支持的类,你就可以利用以下的声明来建设和影射一个工具:
#p#分页标题#e#
FooFileIterator iter = new FooFileIterator(fileLocation, log);
LoadManager manager = new FooFileLoadManager(iter);
SubFooObject obj =
(SubFooObject)ReflectiveObjectLoader.initializeInstance(SubFooObject.class, manager,log);
通过这个处理惩罚,你就建设了一个包括有文件内容的SubFooObject实例。
范围
开拓者必需抉择利用哪个方案来办理问题是最好的;凡是做出这个抉择是最坚苦的部门。在思量利用reflection作数据设置时,你要思量到以下一些限制:
1、不要令一个简朴的问题巨大化。reflection是较量巨大的,因此在须要的时候才利用它。一旦开拓者大白了reflection的本领,他就想利用它来办理所有的问题。假如你有更快、更简朴的方案来办理问题时,你就不该该利用reflection(纵然这个更好的方案大概利用更多的代码)。reflection是强大的,但也有一些风险。
2、思量机能。reflection对机能的影响较量大,因为要在运行时发明和打点类属性需要时间和内存。
从头评估方案
如上所述,利用运行时reflection的第一个限制是“不要令简朴的问题巨大化”。在利用reflection时,这是不行制止的。将reflection和递归团结起来是一个令人头痛的问题;从头看代码也是一件可骇的工作;而精确抉择代码的成果也长短常巨大的。要知道代码的精确浸染的独一要领是利用一些取样数据,逐行地看,就象运行时一样。不外,对付每个大概的数据组合都利用这种方法险些是不行能的。在这种环境下,利用单位测试代码大概有些辅佐,不外也很大概呈现错误。幸运的是,尚有一个可选的要领。
可选的要领
由上面列出的限制可以看到,在某些环境下,利用reflective载入进程大概是得不偿失的。代码发生提供了一个通用的选择要领。你也可以利用reflection来查抄一个类而且为载入进程发生代码。
Andrew Hunt和David Thomas先容了两类的代码发生器,见The Pragmatic Programmer(http://www.javaworld.com/javaworld/jw-11-2001/jw-1102-codegen-p2.html#resources)
1、Passive(被动):被动的代码发生器在实现代码时需要人工的过问。很多的IDE(集成开拓情况)都提供相应的领导来实现。
2、Active(主动):主动的代码发生指的是代码一旦建设,就不再需要修改了。假如有问题发生,这个问题也应该在代码发生器中办理,而不是在发生的源文件中办理。在抱负的环境下,这个进程应该包括在编译的处理惩罚进程中,从而确保类不会逾期。
代码发生的利益和缺点包括有以下方面:
利益:
.简朴:发生的代码凡是是更便于开拓者阅读和调试。
.编译进程的错误:Reflexive在运行时呈现错误的时秘密比编译的期间多。譬喻,改变被载入的工具将有大概令发生的载入类抛出一个编译的错误,不外reflexive进程将不会看到任何的区别,直到在运行时碰着这个类。
缺点:
.维护:利用被动的代码发生,修改被载入的工具将需要更新可能从头发生载入的类。假如该类被从头发生,那么自界说的对象就会丢失。
转头再来看看主动代码发生的长处
在这里我们可以看到在运行时利用reflection是不行以接管的。主动的代码发生有着reflection的全部长处,可是没有它的限制。还可以继承利用reflection,不外只是在代码的发生进程,而不是运行的进程。来由如下:
1、更少冒险:运行时的reflection明明是更冒险的,出格是问题变得巨大的时候。
2、基于单位测试,但并不是编译器
3、多成果性:发生的代码有着runtime reflection的全部长处,并且有着runtime reflection没有的长处。
4、更易懂:固然颠末多次的处理惩罚,可是将递归和reflection团结仍然是很巨大的。发生源代码的方法越发容易表明和领略。代码发生进程需要递归和reflection,但获得的功效是可查察的源代码,而不是难以领略的对象。
写代码发生器
要写一个代码发生器,在思考的时候,你不能只是简朴地编写一个方案来办理一个问题,你应该看得更远。代码发生器(以及reflection)需要你作更多的思考。假如你只是利用runtime reflection,你就不得不在运行时观念化问题,而不是利用简朴、兼容性好的源代码来办理问题。代码发生要求你从两个方面来查察问题。代码发生进程会将抽象的观念转变为实际的源代码。Runtime reflection则一直是抽象的。
#p#分页标题#e#
代码发生进程将你的思考进程转变为代码,然后发生而且编译代码。编译器会让你知道你的思考进程在语法上是否正确;单位测试则可以验证代码在运行时的行为。就动态特性方面,runtime reflection就不能到达这个级此外安详性。
代码发生器
在经验后屡次失败的设计后,我认为最简朴的要领是:在载入进程中,为每一种需要实例化的类发生一个要领。一个要领工场发生每个出格类的正确要领。一个代码编译工具缓冲来自代码工场的要领请求,以发生最终源代码文件的内容。
MethodCode工具是代码发生进程的焦点。以下就是一个int的代码发生工具的例子:
public class MethodForInt extends MethodCode {
private final static MethodParameter param = new MethodParameter(SimpleFileIterator.class, "parser");
public MethodForInt(Class type, CodeBuilder builder){
super(type, builder);
}
public MethodParameter[] getInputParameters(){
return new MethodParameter[]{
param
};
}
public MethodParameter[] getInstanceParameters(){
return getInputParameters();
}
protected String getImplBody(CodeBuilder builder){
return "return " + param.getName() + ".nextInt();
";
}
}
基类MethodCode完玉成部的事情。在代码发生的进程中,MethodCode类抉择要领名字以及用作实现的框架代码。MethodForInt类只需要为它的要领界说所有的数据类型。个中最重要的部门是getImplBody(CodeBuilder builder) 要领。这就是界说函数的处所。getInputParameters()和 getInstanceParameters()这两个要领界说函数的签名。函数签名不单声明白函数,并且还界说了如安在其它函数中挪用它。MethodForInt类在代码发生时发生以下的代码:
/** Generated Load method for int**/
final public static int loadint(com.thoughtworks.rettig.util.SimpleFileIterator parser){
return parser.nextInt();
}
无缝发生
在编译阶段,代码发生为源代码发生带来了特另外承担。你可以利用一个利便的设置编译东西(譬喻Ant)来处理惩罚这个问题。在这里,我要为这篇文章的例子发生代码,我建设了以下的任务:
dir = "."
fork = "yes">
两个参数指定了源代码的目标包,以及用来建设载入进程的类。一旦界说好任务而且将它集成到编译的进程中,代码发生就会成为编译进程的一部门。
比拟事情方案
对付这两个事情方案,我们此刻往返首阐明一下。
当你在运行时碰着问题时,这些方案的真正差异之处是显而易见的。在runtime reflection的方案中,由于遍及地利用reflection和递归,你大概获得的是一个难解的仓库跟踪。发生代码的方法可让你获得一个简朴的仓库跟踪,这样你就可以回溯到发生的源代码作调试。
以下就是一个例子,由同样的错误发生的两种仓库跟踪。我将让你判定一下利用哪一种作调试。(要留意的是为了便于阅读,我已经移除了com.thoughtworks.rettig包的限定)
Runtime Reflection Exception:
java.lang.NumberFormatException: itemName
at java.lang.Integer.parseInt(Integer.java:409)
at java.lang.Integer.parseInt(Integer.java:458)
at ...util.SimpleFileIterator.nextInt(SimpleFileIterator.java:82)
at ...dataLoader.SimpleFileLoadManager$1.load(SimpleFileLoadManager.java:44)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:129)
at ...dataLoader.ReflectiveObjectLoader.constructObject(ReflectiveObjectLoader.java, Compiled Code)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:134)
at ...dataLoader.ReflectiveObjectLoader.constructObjectArray(ReflectiveObjectLoader.java, Compiled Code)
at ...dataLoader.ReflectiveObjectLoader.initializeArray(ReflectiveObjectLoader.java:39)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:123)
at ...dataLoader.ReflectiveObjectLoader.constructObject(ReflectiveObjectLoader.java, Compiled Code)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:134)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:103)
以下是发生代码的Exception
java.lang.NumberFormatException: itemName
at java.lang.Integer.parseInt(Integer.java:409)
at java.lang.Integer.parseInt(Integer.java:458)
at ...util.SimpleFileIterator.nextInt(SimpleFileIterator.java:82)
at ....example.generated.PurchaseOrderLoader.loadint(PurchaseOrderLoader.java:32)
at ....example.generated.PurchaseOrderLoader.loadLineItem(PurchaseOrderLoader.java:22)
at ....example.generated.PurchaseOrderLoader.loadLineItemArray(PurchaseOrderLoader.java, Compiled Code)
at ....example.generated.PurchaseOrderLoader.loadPurchaseOrder(PurchaseOrderLoader.java:27)
#p#分页标题#e#
对付runtime reflection,我们要疏散出问题的话需要作许多的记录日志。在载入的进程中,大量地利用记录日志明明是不适合的。利用reflection,你可以令仓库跟踪越发有意义,不外这会令已经巨大的情况越发巨大化。利用发生代码的要领时,获得的代码只是记下runtime reflection将如那里理惩罚这些景象。
这两种实现方法在机能方面也有着区别。我诧异地发明,在利用runtime reflection时,我的例子载入要慢4到7倍。
一个典范的运行功效如下所示:
java com.thoughtworks.rettig.example.TestPerformance
Number of Iterations: 100000
Generated
Total time: 14481
Max Memory Used: 1337672
Reflection
Total time: 89219
Max Memory Used: 1407944
这个延迟可以归结于在运行时,reflection需要时间来发明类的属性,而发生代码的要领只是由显式的挪用组成。Runtime reflection利用的内存也要多一些,但并不是多许多。虽然,reflection可以作更好的优化,可是该优化将会很是巨大,并且优化的功效大概也远远比不上一个直接的方案。
相反地,发生代码方法的优化是一件垂手可得的工作。在以前的一项目中利用了雷同的代码发生器,我通过优化载入的进程从而利用更少的内存。我只需要几分钟变可以将代码发生器修改好。在优化儿女码发生了一个bug,不外仓库跟踪很直接地指出了代码发生进程中的问题,我很快就纠正过来了。在runtime reflection时,我将不会实验作同样的优化,因为实在是太费劲了。
运行源代码
假如你查察一下源代码,你将可以更好地把握这里谈到的几个问题。要编译和运行源代码,这需要将个中的文件解压到一个空的目次,然后在呼吁行运行ant Install。这样将会利用Ant的编译脚原来发生、编译源代码,而且作单位测试。(这里假定你已经安装了Ant和JUnit 3.7)
我建设了一个例子,它是一个简朴的购置订单,该订单由几类工具构成。JUnit测试案例表明白你如何利用每个要领从一个文件缔造一个购置订单。测试案例然后验证工具的内容,以确保数据被正确地装载。你可以由以下的包中获得测试的内容和所有支持的类:
com.thoughtworks.rettig.example
com.thoughtworks.rettig.example.reflection
com.thoughtworks.rettig.example.generated
在两个测试案例之间最值得留意的差异是runtime reflection无需支持代码来装载数据。这就是reflection的神奇地址。它仅需要类界说和源数据的位置来载入数据,而发生代码的方法在它可以建设测试案例前,需要一个发生的载入类。
在工具建设进程中,两者长短常相似的。以下就是reflection的代码:
SimpleFileIterator iter = new SimpleFileIterator(fileLocation);
LoadManager manager = new SimpleFileLoadManager(iter);
PurchaseOrder obj = (PurchaseOrder) ReflectiveObjectLoader.initializeInstance(PurchaseOrder.class, manager);
以下就是发生的代码:
SimpleFileIterator iter = new SimpleFileIterator(file);
PurchaseOrder po = PurchaseOrderLoader.loadPurchaseOrder(iter);
总结
reflection的长处长短常明明的。当与代码发生结适时,它就成为一个无价的、更重要的、安详的东西。凡是没有其它的方法来举办很多外貌上多余的任务。对付代码发生:我用得越多,就越喜欢它。通过不绝地修改和改造成果,代码变得更为清晰易懂,而runtime reflection的结果则相反,我插手的成果越多,它就变得越巨大。
所以,假如你感想未来要利用reflection来办理一个巨大的问题,要记得以下一条纪律:不要在runtime时做。