Java编程的动态性(3): 应用反射

2024-02-15 17:40

本文主要是介绍Java编程的动态性(3): 应用反射,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

命令行参数处理是一项令人厌烦的零碎工作,不管您过去已经处理过多少次了,它好像总能重新摆在您的面前。与其一遍又一遍地编写同一块代码的不同变种,为什么不利用反射来简化参数处理的工作呢?Java 顾问 Dennis Sosnoski 向您展示了如何做到这一点。在本文中,Dennis 简明扼要地介绍了一个开源库,这个库可以使得命令行参数实际上自己处理自己。

上个月的文章中,我介绍了Java Reflection API,并简要地讲述了它的一些基本功能。我还仔细研究了反射的性能,并且在文章的最后给出了一些指导方针,告诉读者在一个应用程序中何时应该使用反射,何时不应该使用反射。在本月这一期的文章中,我将通过查看一个应用程序来更深入地讨论这一问题,这个应用程序是用于命令行参数处理的一个库,它能够很好地体现反射的强项和弱点。

一开始,在真正进入编写实现代码的工作之前,我将首先定义要解决的问题,然后为这个库设计一个接口。不过,在开发这个库的时候,我并不是按照上述步骤进行的――我先是尽力简化一群有公共代码基础的应用程序中的现有代码,然后使之通用化。本文中使用的“定义-设计-构建”这种线性序列比起完完整整地描述开发过程要简练得多,而且,按照这种方式来组织对开发过程的描述,我可以修正我原先的一些假设,并清理掉这个库的代码中一些不必要的方面。您完全有希望发现将上述方式作为开发您自己的基于反射的应用程序时所使用的模型十分管用。

定义问题

 

我曾经写过许多使用命令行参数的Java应用程序。一开始,大多数应用程序都很小,但最后有些应用程序却变得大到出乎我的意料。下面是我观察到的这些应用程序的变大过程的标准模式:

  1. 一开始只有一个或者两个参数,按照某种特定的顺序排列。
  2. 考虑到这个应用程序有更多的事情要做,于是添加更多的参数。
  3. 厌倦了每次都输入所有的参数,于是让一些参数成为可选的参数,让这些参数带有默认的值。
  4. 忘记了参数的顺序,于是修改代码,允许参数以任何顺序排列。
  5. 将这个应用程序交给其他感兴趣的人。但是他们并不知道这些参数各自代表什么,于是又为这些参数添加更完善的错误检查和“帮助”描述。

当我进入到第5步的时候,我通常会后悔没有将整个过程都放在第一步来做。好在我很快就会忘记后面的那些阶段,不到一两个星期,我又会考虑另外一个简单的小命令行程序,我想拥有这个应用程序。有了这个想法之后,上述整个恶心的循环过程的重现只是时间的问题。

有一些库可以用来帮助进行命令行参数处理。不过,在本文中我会忽略掉这些库,而是自己动手创建一个库。这不是(或者不仅仅是)因为我有着“非此处发明(not invented here)”的态度(即不愿意用外人发明的东西,译者注),而是因为想拿参数处理作为一个实例。这样一来,反射的强项和弱点便正好体现了对参数处理库的需求。特别地,参数处理库:

  • 需要一个灵活的接口,用以支持各种应用程序。
  • 对于每个应用程序,都必须易于配置。
  • 不要求顶级的性能,因为参数只需处理一次。
  • 不存在访问安全性问题,因为命令行应用程序运行的时候通常不带安全管理器。

这个库中实际的反射代码只代表整个实现的一小部分,因此我将主要关注与反射最相关的一些方面。如果您想找到有关这个库的更多内容(或许还想将它用到您自己的简单命令行应用程序中去),您可以在 参考资料部分找到指向Web站点的链接。

草拟出一份设计

应用程序访问参数数据最方便的方式或许是通过该应用程序的 main 对象的一些字段。例如,假设您正在编写一个用于生成业务计划的应用程序。您可能想使用一个 boolean 标记来控制业务计划是简要的还是冗长的,使用一个 int 作为第一年的收入,使用一个 String 作为对产品的描述。我将把这些会影响应用程序的运行的变量称作 形参(parameters),以便与命令行提供的

 实参(arguments)――即形参的值区分开来。通过为这些形参使用字段,将使得在需要形参的应用程序代码中的任何地方都可以方便地调用它们。而且,如果使用字段的话,在定义形参字段时为任意形参设置默认值也很方便,如清单1所示:


清单 1.业务计划生成器(部分清单)

public class PlanGen {private boolean m_isConcise;          // rarely used, default falseprivate int m_initialRevenue = 1000;  // thousands, default is 1Mprivate float m_growthRate = 1.5;     // default is 50% growth rateprivate 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 -- 产品名称

boolean 形参只需标记字符本身就可以设置一个值,而其他类型的形参还需要某种附加的实参信息。对于数值实参,我只将它的值紧跟在形参标记字符之后(这意味着数字不能用作标记字符),而对于带 String 类型值的形参,我将在命令行中使用跟在标记字符后面的实参作为实际的值。最后,如果还需要一些形参(例如业务计划生成器的输出文件的文件名),我假设这些形参的实参值跟在命令行中可选形参值的后面。有了上面给出的这些约定,业务计划生成器的命令行看上去就是这个样子:

java PlanGen -c -f2500 -g2.5 -n "iSue4U - Litigation at Internet Speed" plan.txt

如果把它放在一起,那么每个实参的意思就是:

  • -c -- 生成简要计划
  • -f2500 -- 第一年收入为 $2,500,000
  • -g2.5 -- 每年增长率为250%
  • -n "iSue4U . . ." -- 产品名称是 "iSue4U . . ."
  • plan.txt -- 需要的输出文件名

这时,我已经得到了参数处理库的基本功能的规范说明书。下一步就是为这个应用代码定义一个特定的接口,以使用这个库。

选择接口

您可以使用单个的调用来负责命令行参数的实际处理,但是这个应用程序首先需要以某种方式将它的特定的形参定义到库中。这些形参可以具有不同的几种类型(对于业务计划生成器的例子,形参的类型可以是 boolean , int、 floatjava.lang.String )。每种类型可能又有一些特殊的需求。例如,如果给出了标记字符的话,将 boolean 形参定义为 false 会比较好,而不是总将它定义为 true 。而且,为一个 int 值定义一个有效范围也很有用。

我处理这些不同需求的方法是,首先为所有形参定义使用一个基类,然后为每一种特定类型的形参细分类这个基类。这种方法使得应用程序可以以基本形参定义类的实例数组的形式将形参定义提供给这个库,而实际的定义则可以使用匹配每种形参类型的子类。对于业务计划生成器的例子,这可以采用清单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 neededif (args.length > 0) {// process arguments directly to instancePlanGen inst = new PlanGen();int next = ArgumentProcessor.processArgs(args, PARM_DEFS, inst);// next unused argument is output file nameif (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() 方法,用于处理字段查找。

清单 4. 用于形参定义的基类

public abstract class ParameterDef
{protected char m_char;          // argument flag characterprotected String m_name;        // parameter field nameprotected Field m_field;        // actual parameter fieldprotected 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 accessibilitym_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() 方法在解释完实参值之后,又调用 ArgumentProcessorsetValue() 方法。清单5显示了 ArgumentProcessor 类的不完整版本,包括在构造函数中的形参绑定调用以及 setValue() 方法:

清单 5. 主库类的部分清单

 

public class ArgumentProcessor
{private Object m_targetObject;  // parameter value objectprivate int m_currentIndex;     // current argument position...public ArgumentProcessor(ParameterDef[] parms, Object target) {// bind all parameters to target classfor (int i = 0; i < parms.length; i++) {parms[i].bindToClass(target.getClass());}// save target object for later usem_targetObject = target;}public void setValue(Object value, Field field) {try {// set parameter field value using reflectionfield.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 ,等等)的子类与此十分相似。

清单 6. int 形参定义类

 

public class IntDef extends ParameterDef
{private int m_min;              // minimum allowed valueprivate int m_max;              // maximum allowed valuepublic 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 validatingboolean minus = false;boolean digits = false;int value = 0;// convert number supplied in argument list to 'value'...// make sure we have a valid valuevalue = 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毫秒根本不能引起我的注意。

通过 参考资源中的链接可以找到完整的库代码。它包括我在本文没有提到的一些特性,包括这样一些细节,比如钩子,用于容易地生成一列格式化的形参标记,还有一些描述,有助于为应用程序提供使用指令。欢迎您在自己的程序中使用这个库,并以任何您发现有用的方式扩展这个库。

现在我已讲过了 第1部分中Java类的基础,也讲过了 第2部分中的 Java Reflection API 的原理以及第3部分,本系列剩下的部分将改变话题,讲讲大家不大熟悉的字节码处理。在第4部分,我将从容易的开始,先看看用于使用二进制类的用户友好的 Javassist 库。您是否想转换方法,但是又不愿在字节码中启动程序呢?Javassist 正好适合您的需求。下个月我们将看看如何实现这一点。

这篇关于Java编程的动态性(3): 应用反射的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/712075

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory