我曾经写过许多使用命令行参数的Java应用程序。一开始,大多数应用程序都很小,但最后有些应用程序却变得大到出乎个人意料。下面是我观察到的这些应用程序的变大过程的标准模式: java
当我进入到第5步的时候,我一般会后悔没有将整个过程都放在第一步来作。好在我很快就会忘记后面的那些阶段,不到一两个星期,我又会考虑另一个简单的小命令行程序,我想拥有这个应用程序。有了这个想法以后,上述整个恶心的循环过程的重现只是时间的问题。 git
有一些库能够用来帮助进行命令行参数处理。不过,在本文中我会忽略掉这些库,而是本身动手建立一个库。这不是(或者不只仅是)由于我有着“非此处发明(not invented here)”的态度(即不肯意用外人发明的东西,译者注),而是由于想拿参数处理做为一个实例。这样一来,反射的强项和弱点便正好体现了对参数处理库的需求。特别地,参数处理库: 编程
应用程序访问参数数据最方便的方式或许是经过该应用程序的 main 对象的一些字段。例如,假设您正在编写一个用于生成业务计划的应用程序。您可能想使用一个boolean标记来控制业务计划是简要的仍是冗长的,使用一个int做为第一年的收入,使用一个String做为对产品的描述。我将把这些会影响应用程序的运行的变量称做 形参(parameters),以便与命令行提供的 实参(arguments)――即形参的值区分开来。经过为这些形参使用字段,将使得在 须要形参的应用程序代码中的任何地方均可以方便地调用它们。并且,若是使用字段的话,在定义形参字段时为任意形参设置默认值也很方便,如清单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惯例的一种简化版本。形参的实参值能够以任何顺序提供,在最前面使用一个连字符以指示一个实参给出了一个或者多个单字符的形参标记(与实际的形参的值相 对)。对于这个业务计划生成器,我将采用如下形参标记字符: 函数
boolean形参只需标记字符自己就能够设置一个值,而其余类型的形参还须要某种附加的实参信息。对于数值实参,我只将它的值紧跟在形参标记字符以后 (这意味着数字不能用做标记字符),而对于带String类型值的形参,我将在命令行中使用跟在标记字符后面的实参做为实际的值。最后,若是还须要一些形参(例如业务计划生成器的输出文件的文件名),我假设这些形参的实参值跟在命令行中可选形参值的后面。有了上面给出的这些约定,业务计划生成器的命令行看上去就是这个样子: 性能
java PlanGen -c -f2500 -g2.5 -n "iSue4U - Litigation at Internet Speed" plan.txt 测试
若是把它放在一块儿,那么每一个实参的意思就是: ui
这时,我已经获得了参数处理库的基本功能的规范说明书。下一步就是为这个应用代码定义一个特定的接口,以使用这个库。 spa
您可使用单个的调用来负责命令行参数的实际处理,可是这个应用程序首先须要以某种方式将它的特定的形参定义到库中。这些形参能够具备不一样的几种类型(对于业务计划生成器的例子,形参的类型能够是boolean,int、float和java.lang.String)。每种类型可能又有一些特殊的需求。例如,若是给出了标记字符的话,将boolean形参定义为false会比较好,而不是总将它定义为true。并且,为一个int值定义一个有效范围也颇有用。
我处理这些不一样需求的方法是,首先为全部形参定义使用一个基类,而后为每一种特定类型的形参细分类这个基类。这种方法使得应用 程序能够以基本形参定义类的实例数组的形式将形参定义提供给这个库,而实际的定义则可使用匹配每种形参类型的子类。对于业务计划生成器的例子,这能够采 用清单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所示:
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"); } } }
最后剩下的部分就是处理错误报告(例如一个未知的形参标记字符或者一个超出范围的数字值)。出于这个目的,我将定义ArgumentErrorException做为一个未经检查的异常,若是出现了某个这一类的错误,就将抛出这个异常。若是这个异常没有被捕捉到,它将当即关闭应用程序,并将一条错误消息和栈跟踪 输出到控制台。一个替代的方法是,您也能够在代码中直接捕捉这个异常,而且用其余方式处理异常(例如,可能会与使用信息一块儿输出真正的错误消息)。
为了让这个库像计划的那样使用反射,它须要查找由形参定义数组指定的一些字段,而后将适当的值存到这些来自相应的命令行参数的字段中。这项任务能够经过只查找实际的命令行参数所需的字段信息来处理,可是我反而选择将查找和使用分开。我将预先找到全部的字段,而后 只使用在参数处理期间已经被找到的信息。
预先找到全部的字段是一种防错性编程的步骤,这样作能够消除使用反射时带来的一个潜在的问题。若是我只是查找须要的字段,那么就很容易破坏一个形参定义(例如,输错相应的字段名),并且还不能认识到有错误发生。这里不会有编译时错误,由于字段名是做为String传递的,并且,只要命令行没有指定与已破坏的形参定义相匹配的实参,程序也能够执行得很好。这种被蒙蔽的错误很容易致使不完善代码的发布。
假设我想在实际处理实参以前查找字段信息,清单4显示了用于形参定义的基类的实现,这个实现带有一个bindToClass()方法,用于处理字段查找。
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); }
实际的库实现还涉及到本文没有说起的几个类。我不打算一一介绍每个类,由于其中大部分类都与库的反射方面不相关。我将提到的是,我选择将目标对象存为ArgumentProcessor类的一个字段,并在这个类中实现一个形参字段的真正设置。这种方法为参数处理提供了一个简单的模式:ArgumentProcessor类扫描实参以发现形参标记,为每一个标记查找相应的形参定义(老是ParameterDef的一个子类),再调用这个定义的handle()方法。handle()方法在解释完实参值以后,又调用ArgumentProcessor的setValue()方法。清单5显示了ArgumentProcessor类的不完整版本,包括在构造函数中的形参绑定调用以及setValue()方法:
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显示了int形参值的形参定义子类的部分实现。这包括对基类的bindToClass()方法(来自 清单4)的重载,这个重载的方法首先调用基类的实现,而后检查找到的字段是否匹配预期的类型。其余特定形参类型(boolean、float、String,等等)的子类与此十分类似。
public class IntDef extends ParameterDef { private int m_min; // minimum allowed value private int m_max; // maximum allowed value public IntDef(char chr, String name, int min, int max) { super(chr, name); m_min = min; m_max = max; } protected void bindToClass(Class clas) { super.bindToClass(clas); Class type = m_field.getType(); if (type != Integer.class && type != Integer.TYPE) { throw new IllegalArgumentException("Field '" + m_name + "'in " + clas.getName() + " is not of type int"); } } public void handle(ArgumentProcessor proc) { // set up for validating boolean minus = false; boolean digits = false; int value = 0; // convert number supplied in argument list to 'value' ... // make sure we have a valid value value = minus ? -value : value; if (!digits) { proc.reportArgumentError(m_char, "Missing value"); } else if (value < m_min || value > m_max) { proc.reportArgumentError(m_char, "Value out of range"); } else { proc.setValue(new Integer(value), m_field); } } }
在本文中,我讲述了一个用于处理命令行参数的库的设计过程,做为反射的一个实际的例子。这个库很好地阐明了如何有效地使用反射――它简化应用程序的代码, 并且不用明显地牺牲性能。牺牲了多少性能呢?从对个人开发系统的一些快速测试中能够看出,一个简单的测试程序在使用整个库进行了参数处理时比起不带任何参 数处理时运行起来平均只慢40毫秒。多出来的这些时间大部分是花在库类和库代码所使用的其余类的装载上,所以,即便是对于那些定义了许多命令行形参和许多 实参值的应用程序,也不大可能会比这一结果糟不少。对于个人命令行应用程序,额外的40毫秒根本不能引发个人注意。