免职声明:本网站为公益性网站,部分信息来自网络,如果涉及贵网站的知识产权,请及时反馈,我们承诺第一时间删除!
This website is a public welfare website, part of the information from the Internet, if it involves the intellectual property rights of your website, please timely feedback, we promise to delete the first time.
电话Tel: 19550540085: QQ号: 929496072 or 邮箱Email: Lng@vip.qq.com
摘要:在 上一篇文章中,我介绍了Java Reflection API,并简要地讲述了它的一些基本功能。我还仔细研究了反射的性能,并且在文章的最后给出了一些指导方针,告诉读者在一个应用程序中何时应该使用反射,何时不应该使用反射。在本月这一期的文章中,我将通过查看一个应用程..
在 上一篇文章中,我介绍了Java Reflection API,并简要地讲述了它的一些基本功能。我还仔细研究了反射的性能,并且在文章的最后给出了一些指导方针,告诉读者在一个应用程序中何时应该使用反射,何时不应该使用反射。在本月这一期的文章中,我将通过查看一个应用程序来更深入地讨论这一问题,这个应用程序是用于命令行参数处理的一个库,它能够很好地体现反射的强项和弱点。 一开始,在真正进入编写实现代码的工作之前,我将首先定义要解决的问题,然后为这个库设计一个接口。不过,在开发这个库的时候,我并不是按照上述步骤进行的――我先是尽力简化一群有公共代码基础的应用程序中的现有代码,然后使之通用化。本文中使用的“定义-设计-构建”这种线性序列比起完完整整地描述开发过程要简练得多,而且,按照这种方式来组织对开发过程的描述,我可以修正我原先的一些假设,并清理掉这个库的代码中一些不必要的方面。您完全有希望发现将上述方式作为开发您自己的基于反射的应用程序时所使用的模型十分管用。 我曾经写过许多使用命令行参数的Java应用程序。一开始,大多数应用程序都很小,但最后有些应用程序却变得大到出乎我的意料。下面是我观察到的这些应用程序的变大过程的标准模式: 1. 一开始只有一个或者两个参数,按照某种特定的顺序排列。 2. 考虑到这个应用程序有更多的事情要做,于是添加更多的参数。 3. 厌倦了每次都输入所有的参数,于是让一些参数成为可选的参数,让这些参数带有默认的值。 4. 忘记了参数的顺序,于是修改代码,允许参数以任何顺序排列。 5. 将这个应用程序交给其他感兴趣的人。但是他们并不知道这些参数各自代表什么,于是又为这些参数添加更完善的错误检查和“帮助”描述。 当我进入到第5步的时候,我通常会后悔没有将整个过程都放在第一步来做。好在我很快就会忘记后面的那些阶段,不到一两个星期,我又会考虑另外一个简单的小命令行程序,我想拥有这个应用程序。有了这个想法之后,上述整个恶心的循环过程的重现只是时间的问题。 有一些库可以用来帮助进行命令行参数处理。不过,在本文中我会忽略掉这些库,而是自己动手创建一个库。这不是(或者不仅仅是)因为我有着“非此处发明(not invented here)”的态度(即不愿意用外人发明的东西,译者注),而是因为想拿参数处理作为一个实例。这样一来,反射的强项和弱点便正好体现了对参数处理库的需求。特别地,参数处理库: · 需要一个灵活的接口,用以支持各种应用程序。 · 对于每个应用程序,都必须易于配置。 · 不要求顶级的性能,因为参数只需处理一次。 · 不存在访问安全性问题,因为命令行应用程序运行的时候通常不带安全管理器。 这个库中实际的反射代码只代表整个实现的一小部分,因此我将主要关注与反射最相关的一些方面。如果您想找到有关这个库的更多内容(或许还想将它用到您自己的简单命令行应用程序中去),您可以在 参考资料部分找到指向Web站点的链接。 草拟出一份设计应用程序访问参数数据最方便的方式或许是通过该应用程序的 main 对象的一些字段。例如,假设您正在编写一个用于生成业务计划的应用程序。您可能想使用一个 清单 1.业务计划生成器(部分清单)public class PlanGen { private boolean m_isConcise; // rarely used, default false private int m_initialRevenue = 1000; // thousands, default is 1M private float m_growthRate = 1.5; // default is 50% growth rate private String m_productDescription = // McD look out, here I come "eFood - (Really) Fast Food Online"; ... private int revenueForYear(int year) { return (int)(m_initialRevenue * Math.pow(m_growthRate, year-1)); } ... 反射将使得应用程序可以直接访问这些私有字段,允许参数处理库在应用程序代码中没有任何特殊钩子的情况下设置参数的值。但是我 的确需要某种方法能让这个库将这些字段与特定的命令行参数相关起来。在我能够定义一个参数和一个字段之间的这种关联如何与库进行通信之前,我需要决定我希望如何格式化这些命令行参数。 对于本文,我将定义一种命令行格式,这是UNIX惯例的一种简化版本。形参的实参值可以以任何顺序提供,在最前面使用一个连字符以指示一个实参给出了一个或者多个单字符的形参标记(与实际的形参的值相 对)。对于这个业务计划生成器,我将采用以下形参标记字符: · c-- 简要计划 · f-- 第一年收入(千美元) · g-- 增长率(每年) · n-- 产品名称
如果把它放在一起,那么每个实参的意思就是: · · · · · 这时,我已经得到了参数处理库的基本功能的规范说明书。下一步就是为这个应用代码定义一个特定的接口,以使用这个库。 选择接口您可以使用单个的调用来负责命令行参数的实际处理,但是这个应用程序首先需要以某种方式将它的特定的形参定义到库中。这些形参可以具有不同的几种类型(对于业务计划生成器的例子,形参的类型可以是 我处理这些不同需求的方法是,首先为所有形参定义使用一个基类,然后为每一种特定类型的形参细分类这个基类。这种方法使得应用程序可以以基本形参定义类的实例数组的形式将形参定义提供给这个库,而实际的定义则可以使用匹配每种形参类型的子类。对于业务计划生成器的例子,这可以采用清单2中所示的形式: 清单 2. 业务计划生成器的形参定义private static final ParameterDef[] PARM_DEFS = { new BoolDef('c', "m_isConcise"), new IntDef('f', "m_initialRevenue", 10, 10000), new FloatDef('g', "m_growthRate", 1.0, 100.0), new StringDef('n', "m_productDescription") } 有了得到允许的在一个数组中定义的形参,应用程序对参数处理代码的调用就可以像对一个静态方法的单个调用一样简单。为了允许除形参数组中定义的实参之外额外的实参(要么是必需的值,要么是可变长度的值),我将令这个调用返回被处理实参的实际数量。这样应用程序便可以检查额外的实参并适当地使用它们。最后的结果看上去如清单3所示: 清单 3. 使用库public class PlanGen { private static final ParameterDef[] PARM_DEFS = { ... }; public static void main(String[] args) { // if no arguments are supplied, assume help is needed if (args.length > 0) { // process arguments directly to instance PlanGen inst = new PlanGen(); int next = ArgumentProcessor.processArgs (args, PARM_DEFS, inst); // next unused argument is output file name if (next >= args.length) { System.err.println("Missing required output file name"); System.exit(1); } File outf = new File(args[next++]); ... } else { System.out.println("\nUsage: java PlanGen " + "[-options] file\nOptions are:\n c concise plan\n" + "f first year revenue (K$)\n g growth rate\n" + "n product description"); } } } 最后剩下的部分就是处理错误报告(例如一个未知的形参标记字符或者一个超出范围的数字值)。出于这个目的,我将定义 实现库为了让这个库像计划的那样使用反射,它需要查找由形参定义数组指定的一些字段,然后将适当的值存到这些来自相应的命令行参数的字段中。这项任务可以通过只查找实际的命令行参数所需的字段信息来处理,但是我反而选择将查找和使用分开。我将预先找到所有的字段,然后 只使用在参数处理期间已经被找到的信息。 预先找到所有的字段是一种防错性编程的步骤,这样做可以消除使用反射时带来的一个潜在的问题。如果我只是查找需要的字段,那么就很容易破坏一个形参定义(例如,输错相应的字段名),而且还不能认识到有错误发生。这里不会有编译时错误,因为字段名是作为 假设我想在实际处理实参之前查找字段信息,清单4显示了用于形参定义的基类的实现,这个实现带有一个 清单 4. 用于形参定义的基类public abstract class ParameterDef { protected char m_char; // argument flag character protected String m_name; // parameter field name protected Field m_field; // actual parameter field protected ParameterDef(char chr, String name) { m_char = chr; m_name = name; } public char getFlag() { return m_char; } protected void bindToClass(Class clas) { try { // handle the field look up and accessibility m_field = clas.getDeclaredField(m_name); m_field.setAccessible(true); } catch (NoSuchFieldException ex) { throw new IllegalArgumentException("Field '" + m_name + "' not found in " + clas.getName()); } } public abstract void handle(ArgumentProcessor proc); } 实际的库实现还涉及到本文没有提及的几个类。我不打算一一介绍每一个类,因为其中大部分类都与库的反射方面不相关。我将提到的是,我选择将目标对象存为 清单 5. 主库类的部分清单public class ArgumentProcessor { private Object m_targetObject; // parameter value object private int m_currentIndex; // current argument position ... public ArgumentProcessor(ParameterDef[] parms, Object target) { // bind all parameters to target class for (int i = 0; i < parms.length; i++) { parms[i].bindToClass(target.getClass()); } // save target object for later use m_targetObject = target; } public void setValue(Object value, Field field) { try { // set parameter field value using reflection field.set(m_targetObject, value); } catch (IllegalAccessException ex) { throw new IllegalArgumentException("Field " + field.getName() + " is not accessible in object of class " + m_targetObject.getClass().getName()); } } public void reportArgumentError(char flag, String text) { throw new ArgumentErrorException(text + " for argument '" + flag + "' in argument " + m_currentIndex); } public static int processArgs(String[] args, ParameterDef[] parms, Object target) { ArgumentProcessor inst = new ArgumentProcessor(parms, target); ... } } 最后,清单6显示了 清单 6. |