Java 编程的动态性,第3部分: 应用反射--转载
在?中,我介绍了Java Reflection API,并简要地讲述了它的一些基本功能。我还仔细研究了反射的性能,并且在文章的最后给出了一些指导方针,告诉读者在一个应用程序中何时应该使用反射,何时不应该使用反射。在本月这一期的文章中,我将通过查看一个应用程序来更深入地讨论这一问题,这个应用程序是用于命令行参数处理的一个库,它能够很好地体现反射的强项和弱点。 一开始,在真正进入编写实现代码的工作之前,我将首先定义要解决的问题,然后为这个库设计一个接口。不过,在开发这个库的时候,我并不是按照上述步骤进行的――我先是尽力简化一群有公共代码基础的应用程序中的现有代码,然后使之通用化。本文中使用的“定义-设计-构建”这种线性序列比起完完整整地描述开发过程要简练得多,而且,按照这种方式来组织对开发过程的描述,我可以修正我原先的一些假设,并清理掉这个库的代码中一些不必要的方面。您完全有希望发现将上述方式作为开发您自己的基于反射的应用程序时所使用的模型十分管用。 我曾经写过许多使用命令行参数的Java应用程序。一开始,大多数应用程序都很小,但最后有些应用程序却变得大到出乎我的意料。下面是我观察到的这些应用程序的变大过程的标准模式:
当我进入到第5步的时候,我通常会后悔没有将整个过程都放在第一步来做。好在我很快就会忘记后面的那些阶段,不到一两个星期,我又会考虑另外一个简单的小命令行程序,我想拥有这个应用程序。有了这个想法之后,上述整个恶心的循环过程的重现只是时间的问题。 有一些库可以用来帮助进行命令行参数处理。不过,在本文中我会忽略掉这些库,而是自己动手创建一个库。这不是(或者不仅仅是)因为我有着“非此处发明(not invented here)”的态度(即不愿意用外人发明的东西,译者注),而是因为想拿参数处理作为一个实例。这样一来,反射的强项和弱点便正好体现了对参数处理库的需求。特别地,参数处理库:
这个库中实际的反射代码只代表整个实现的一小部分,因此我将主要关注与反射最相关的一些方面。如果您想找到有关这个库的更多内容(或许还想将它用到您自己的简单命令行应用程序中去),您可以在?部分找到指向Web站点的链接。 应用程序访问参数数据最方便的方式或许是通过该应用程序的 main 对象的一些字段。例如,假设您正在编写一个用于生成业务计划的应用程序。您可能想使用一个? 反射将使得应用程序可以直接访问这些私有字段,允许参数处理库在应用程序代码中没有任何特殊钩子的情况下设置参数的值。但是我?的确需要某种方法能让这个库将这些字段与特定的命令行参数相关起来。在我能够定义一个参数和一个字段之间的这种关联如何与库进行通信之前,我需要决定我希望如何格式化这些命令行参数。 对于本文,我将定义一种命令行格式,这是UNIX惯例的一种简化版本。形参的实参值可以以任何顺序提供,在最前面使用一个连字符以指示一个实参给出了一个或者多个单字符的形参标记(与实际的形参的值相 对)。对于这个业务计划生成器,我将采用以下形参标记字符:
如果把它放在一起,那么每个实参的意思就是:
-c ?-- 生成简要计划
这时,我已经得到了参数处理库的基本功能的规范说明书。下一步就是为这个应用代码定义一个特定的接口,以使用这个库。
您可以使用单个的调用来负责命令行参数的实际处理,但是这个应用程序首先需要以某种方式将它的特定的形参定义到库中。这些形参可以具有不同的几种类型(对于业务计划生成器的例子,形参的类型可以是? 我处理这些不同需求的方法是,首先为所有形参定义使用一个基类,然后为每一种特定类型的形参细分类这个基类。这种方法使得应用程序可以以基本形参定义类的实例数组的形式将形参定义提供给这个库,而实际的定义则可以使用匹配每种形参类型的子类。对于业务计划生成器的例子,这可以采用清单2中所示的形式: 有了得到允许的在一个数组中定义的形参,应用程序对参数处理代码的调用就可以像对一个静态方法的单个调用一样简单。为了允许除形参数组中定义的实参之外额外的实参(要么是必需的值,要么是可变长度的值),我将令这个调用返回被处理实参的实际数量。这样应用程序便可以检查额外的实参并适当地使用它们。最后的结果看上去如清单3所示:
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] filenOptions are:n c concise plann" +
"f first year revenue (K$)n g growth raten" +
"n product description");
}
}
} 最后剩下的部分就是处理错误报告(例如一个未知的形参标记字符或者一个超出范围的数字值)。出于这个目的,我将定义
为了让这个库像计划的那样使用反射,它需要查找由形参定义数组指定的一些字段,然后将适当的值存到这些来自相应的命令行参数的字段中。这项任务可以通过只查找实际的命令行参数所需的字段信息来处理,但是我反而选择将查找和使用分开。我将预先找到所有的字段,然后 只使用在参数处理期间已经被找到的信息。 预先找到所有的字段是一种防错性编程的步骤,这样做可以消除使用反射时带来的一个潜在的问题。如果我只是查找需要的字段,那么就很容易破坏一个形参定义(例如,输错相应的字段名),而且还不能认识到有错误发生。这里不会有编译时错误,因为字段名是作为? 假设我想在实际处理实参之前查找字段信息,清单4显示了用于形参定义的基类的实现,这个实现带有一个?
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);
} 实际的库实现还涉及到本文没有提及的几个类。我不打算一一介绍每一个类,因为其中大部分类都与库的反射方面不相关。我将提到的是,我选择将目标对象存为?
// 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?形参定义类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毫秒根本不能引起我的注意。 通过?中的链接可以找到完整的库代码。它包括我在本文没有提到的一些特性,包括 这样一些细节,比如钩子,用于容易地生成一列格式化的形参标记,还有一些描述,有助于为应用程序提供使用指令。欢迎您在自己的程序中使用这个库,并以任何您发现有用的方式扩展这个库。 现在我已讲过了?中Java类的基础,也讲过了?中的 Java Reflection API 的原理以及第3部分,本系列剩下的部分将改变话题,讲讲大家不大熟悉的字节码处理。在第4部分,我将从容易的开始,先看看用于使用二进制类的用户友好的 Javassist 库。您是否想转换方法,但是又不愿在字节码中启动程序呢?Javassist 正好适合您的需求。下个月我们将看看如何实现这一点。 原文:http://www.ibm.com/developerworks/cn/java/j-dyn0715/index.html (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |