勇于挑战
不畏艰辛
posts - 22,comments - 0,trackbacks - 0
摘要

  开发者通过各种各样的方法来尝试避免单调冗余的编程。一些编程的规则例如继承、多态或者设计模型可以帮助开发者避免产生多余的代码。不过由于软件开发方面存在着不确定性,因此这些规则并不能消除代码维护和重新编写的需要。在很多时候维护都是不可避免的,只有不能运作的软件才是从不需要维护的。不过,这篇文章介绍了你可以使用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,文档类型定义),文件格式等。在这个例子中,映射将一个对象的类定义解释为我们要映射的流程。映射可以在运行时复制代码的功能。在需要重写代码时,我将载入的过程用映射来代替,它所需要的时间和重写是一样的。

  载入的工程可以概括为以下几步:

  1、解释:一个影射决定你在构造一个对象时需要些什么

  2、请求数据:要满足构造的需要,要进行一个调用来得到数据

  3、拖:数据由源中得到。

  4、推:数据被填充入一个对象的新实例

  5、如果必要的话,重复步骤1

  你需要以下的类来满足以上的步骤:

  .数据类(Data classes):由ASCII文件中的数据实例化。类定义提供数据的影射。数据类必须满足以下的条件:

  .它们必须包含有一个构造器来接收全部必需的参数,以使用一个有效的状态来构造对象;

  .它们必须由对象构成,这些对象是reflective过程知道如何处理的

  .对象装载器(Object loader):使用reflection和数据类作为一个影射来载入数据。产生数据请求。

  .载入管理器(Load manager):作为对象装载器和数据源的中介层,将对数据的请求转换为一个数据指定的调用。这可以令对象载入器做到与数据源无关。通过它的接口和一个可载入的类对象通信。

  .数据循环接口(Data iterator interface):载入管理器和载入类对象使用这个接口来由数据源中得到数据。

  一旦你创建了支持的类,你就可以使用以下的声明来创建和影射一个对象:

FooFileIterator iter = new FooFileIterator(fileLocation, log);
LoadManager manager = new FooFileLoadManager(iter);
SubFooObject obj =
(SubFooObject)ReflectiveObjectLoader.initializeInstance(SubFooObject.class, manager,log);

  通过这个处理,你就创建了一个包含有文件内容的SubFooObject实例。


作者:QQ新人类出处:YESKY责任编辑: 方舟 [ 2002-04-14 09:51 ]
开发者通过各种各样的方法来尝试避免单调冗余的编程。一些编程的规则例如继承、多态或者设计模型可以帮助开发者避免产生多余的代码

  局限

  开发者必须决定使用哪个方案来解决问题是最好的;通常做出这个决定是最困难的部分。在考虑使用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,但得到的结果是可查看的源代码,而不是难以理解的东西。


作者:QQ新人类出处:YESKY责任编辑: 方舟 [ 2002-04-14 09:51 ]
开发者通过各种各样的方法来尝试避免单调冗余的编程。一些编程的规则例如继承、多态或者设计模型可以帮助开发者避免产生多余的代码

  写代码产生器

  要写一个代码产生器,在思考的时候,你不能只是简单地编写一个方案来解决一个问题,你应该看得更远。代码产生器(以及reflection)需要你作更多的思考。如果你只是使用runtime reflection,你就不得不在运行时概念化问题,而不是使用简单、兼容性好的源代码来解决问题。代码产生要求你从两个方面来查看问题。代码产生过程会将抽象的概念转变为实际的源代码。Runtime reflection则一直是抽象的。

  代码产生过程将你的思考过程转变为代码,然后产生并且编译代码。编译器会让你知道你的思考过程在语法上是否正确;单元测试则可以验证代码在运行时的行为。就动态特性方面,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();\n";
}
}

  基类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)

  对于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时做。

posted on 2009-05-14 16:31 郭鹏 阅读(148) 评论(0)  编辑  收藏 所属分类: JAVA