Jakarta Commons项目组介绍
Jakarta Commons:巧用类和组件
Jakarta Commons 是Jakarta 的子项目,它创建和维护着许多独立软件包,这些包一般与其他框架或产品
无关,其中收集了大量小型、实用的组件,大部分面向服务器端编程。
Commons的包分成两部分:Sandbox,Commons 代码库。Sandbox 是一个测试平台,用来检验各种设想、
计划。本文介绍的组件属于Commons代码库,文章将展示各个组件的功能、适用场合,并通过简单的例子
介绍其用法。
一、概述
可重用性是Jakarta Commons 项目的灵魂所在。这些包在设计阶段就已经考虑了可重用性问题。其中
一些包,例如Commons 里面用来记录日志的Logging包,最初是为其他项目设计的,例如Jakarta Struts
项目,当人们发现这些包对于其他项目也非常有用,能够极大地帮助其他项目的开发,他们决定为这些包
构造一个"公共"的存放位置,这就是Jakarta Commons项目。
为了真正提高可重用性,每一个包都必须不依赖于其他大型的框架或项目。因此,Commons项目的包
基本上都是独立的,不仅是相对于其他项目的独立,而且相对于Commons内部的大部分其他包独立。虽然
存在一些例外的情况,例如Betwixt 包要用到XML API,但绝大部分只使用最基本的API,其主要目的就
是要能够通过简单的接口方便地调用。
不过由于崇尚简洁,许多包的文档变得过于简陋,缺乏维护和支持,甚至有一部分还有错误的链接,
文档也少得可怜。大部分的包需要我们自己去找出其用法,甚至有时还需要我们自己去分析其适用场合。
本文将逐一介绍这些包,希望能够帮助你迅速掌握这一积累了许多人心血的免费代码库。
说明:Jakarta Commons 和Apache Commons 是不同的,后者是Apache Software Foundation的一个
顶层项目,前者则是Jakarta
项目的一个子项目,同是也是本文要讨论的主角。本文后面凡是提到Commons
的地方都是指Jakarta 的Commons。
为了便于说明,本文把Commons 项目十八个成品级的组件(排除了EL、Latka和Jexl)分成5类,
如下表所示。
必须指出的是,这种分类只是为了方便文章说明,Commons 项目里面实际上并不存在这种分类,同时
这些分类的边界有时也存在一定的重叠。
本文首先介绍Web 相关类和其他类里面的组件,下一篇文章将涉及XML 相关、包装这两类,最后一篇
文章专门介绍属于工具类的包。
二、其他类
CLI、Discovery、Lang 和Collections 包归入其他类,这是因为它们都各自针对某个明确、实用的
小目标,可谓专而精。
2.1 CLI
■ 概况:CLI 即Command Line Interface,也就是"命令行接口",它为Java 程序访问和解析命令行
参数提供了一种统一的接口。
■ 官方资源:主页,二进制,源代码
■ 何时适用:当你需要以一种一致的、统一的方式访问命令行参数之时。
■ 示例应用:CLIDemo.java。CLASSPATH 中必须包含commons-cli-1.0.jar。
■ 说明:
有多少次你不得不为一个新的应用程序重新设计新的命令行参数处理方式?如果能够只用某个单一
的接口,统一完成诸如定义输入参数(是否为强制参数,数值还是字符串,等等)、根据一系列规则分析
参数、确定应用要采用的路径等任务,那该多好!答案就在CLI。
在CLI中,每一个想要在命令中指定的参数都是一个Option对象。首先创建一个Options 对象,将
各个Option对象加入Options对象,然后利用CLI提供的方法来解析用户的输入参数。Option对象可以
要求用户必须输入某个参数,例如必须在命令行提供文件名字。如果某个参数是必须的,创建Option 对
象的时候就要显式地指定。
下面是使用CLI 的步骤。
// …
// ① 创建一个Options:
Options options = new Options();
options.addOption("t", false, "current time");
// …
// ② 创建一个解析器,分析输入:
CommandLineParser parser = new BasicParser();
CommandLine cmd;
try {
cmd = parser.parse(options, args);
} catch (ParseException pe) {
usage(options);
return;
}
// …
// ③ 最后就可以根据用户的输入,采取相应的操作:
if (cmd.hasOption("n")) {
System.err.println("Nice to meet you: " +
cmd.getOptionValue('n'));
}
这就是使用CLI的完整过程了。当然,CLI 还提供了其他高级选项,例如控制格式和解析过程等,
但基本的使用思路仍是一致的。请参见本文最后提供的示例程序。
2.2 Discovery
■ 概况:Discovery 组件是发现模式(Discovery Pattern)的一个实现,它的目标是按照一种统一
的方式定位和实例化类以及其他资源。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:当你想用最佳的算法在Java程序中查找Java接口的各种实现之时。
■ 应用实例:DiscoveryDemo.java,MyInterface.java,MyImpl1.java,MyImpl2.java,MyInterface。
要求CLASSPATH 中必须包含commons-discovery.jar和commons-logging.jar。
■ 说明:
Discovery 的意思就是"发现",它试图用最佳的算法查找某个接口的所有已知的实现。在使用服务的
场合,当我们想要查找某个服务的所有已知的提供者时,Discovery 组件尤其有用。
考虑一下这种情形:我们为某个特别复杂的任务编写了一个接口,所有该接口的实现都用各不相同的
方式来完成这个复杂任务,最终用户可以根据需要来选择完成任务的具体方式。那么,在这种情形下,
最终用户应该用什么办法来找出接口的所有可用实现(即可能的完成任务的方式)呢?
上面描述的情形就是所谓的服务-服务提供者体系。服务的功能由接口描述,服务提供者则提供具体
的实现。现在的问题是最终用户要用某种办法来寻找系统中已经安装了哪些服务提供者。在这种情形下,
Discovery 组件就很有用了,它不仅可以用来查找那些实现了特定接口的类,而且还可以用来查找资源,
例如图片或其他文件等。在执行这些操作时,Discovery遵从Sun的服务提供者体系所定义的规则。
由于这个原因,使用Discovery 组件确实带来许多方便。请读者参阅本文后面示例程序中的接口
MyInterface.java
和两个实现类MyImpl1.java、MyImple2.java,了解下面例子的细节。在使用Discovery
的时候要提供MyInterface 文件,把它放入META-INF/services目录,注意该文件的名字对应接口的完整
限定名称(Fully Qualified Name),如果接口属于某个包,该文件的名字也必须相应地改变。
// …
// ① 创建一个类装入器的实例。
ClassLoaders loaders =
ClassLoaders.getAppLoaders(MyInterface.class, getClass(), false);
// …
// ② 用DiscoverClass 的实例来查找实现类。
DiscoverClass discover = new DiscoverClass(loaders);
// …
// ③ 查找实现了指定接口的类:
Class implClass = discover.find(MyInterface.class);
System.err.println("Implementing Provider: " + implClass.getName());
运行上面的代码,就可以得到在MyInterface 文件中注册的类。再次提醒,如果你的实现是封装在包
里面的,在这里注册的名字也应该作相应地修改,如果该文件没有放在正确的位置,或者指定名字的实现
类不能找到或实例化,程序将抛出DiscoverException,表示找不到符合条件的实现。下面是MyInterface
文件内容的一个例子:MyImpl2 # Implementation 2。
当然,实现类的注册办法并非只有这么一种,否则的话Discovery 的实用性就要大打折扣了!实际上,
按照Discovery 内部的类查找机制,按照这种方法注册的类将是Discovery 最后找到的类。另一种常用的
注册方法是通过系统属性或用户定义的属性来传递实现类的名字,例如,放弃
META-INF/services 目录下
的文件,改为执行java -DMyInterface=MyImpl1 DiscoveryDemo命令来运行示例程序,这里的系统属性
是接口的名字,值是该接口的提供者,运行的结果是完全一样的。
Discovery 还可以用来创建服务提供者的(singleton)实例并调用其方法,语法如下:
((MyInterface)discover.newInstance(MyInterface.class)).myMethod();。注意在这个例子中,我们并
不知道到底哪一个服务提供者实现了myMethod,甚至我们根本不必关心这一点。具体的情形与运行这段
代码的方式以及运行环境中已经注册了什么服务提供者有关,在不同的环境下运行,实际得到的服务提供
者可能不同。
2.3 Lang
■ 概况:Lang是java.lang 的一个扩展包,增加了许多操作String的功能,另外还支持C 风格的枚举量
。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:当java.lang 包提供的方法未能满足需要,想要更多的功能来处理String、数值和
System 属性时;还有,当你想要使用C风格的枚举量时。
■ 示例应用:LangDemo.java,Mortgage.java,OnTV.java。CLASSPATH中必须包含commons-lang.jar。
■ 说明:
这个包提供了许多出于方便目的而提供的方法,它们中的大多数是静态的,简化了日常编码工作。
StringUtils类是其中的一个代表,它使得开发者能够超越标准的java.lang.String 包来处理字符串。
使用这些方法很简单,通常只要在调用静态方法时提供适当的参数就可以了。例如,如果要将某个单词
的首字符改为大写,只需调用:StringUtils.capitalise("name"),调用的输出结果是Name。请浏览
StringUtils API 文档了解其他静态方法,也许你会找到一些可以直接拿来使用的代码。本文提供的示例
程序示范了其中一些方法的使用。
另一个值得注意的类是RandomStringUtils,它提供了生成随机字符串的方法,用来创建随机密码实
在太方便了。
NumberUtils 类提供了处理数值数据的方法,许多方法值得一用,例如寻找最大、最小数的方法,将
String 转换成数值的方法,等等。NumberRange和CharRange类分别提供了创建和操作数值范围、字符范
围的方法。
Builder包里的类提供了一些特殊的方法,可用来构造类的toString、hashCode、compareTo 和equals
方法,其基本思路就是构造出类的高质量的toString、hashCode、compareTo 和equals 方法,从而免去
了用户自己定义这些方法之劳,只要调用一下Builder 包里面的方法就可以了。例如,我们可以用
ToStringBuilder 来构造出类的toString描述,如下例所示:
public class Mortgage {
private float rate;
private int years;
....
public String toString() {
return new ToStringBuilder(this).
append("rate", this.rate).
append("years", this.years).
toString();
}
}
使用这类方法有什么好处呢?显然,它使得我们有可能通过一种统一的方式处理所有数据类型。所有
Builder 方法的用法都和上例相似。
Java 没有C 风格的枚举量,为此,lang 包提供了一个类型安全的Enum 类型,填补了空白。Enum 类
是抽象的,如果你要创建枚举量,就要扩展Enum 类。下面的例子清楚地说明了Enum 的用法。
import org.apache.commons.lang.enum.Enum;
import java.util.Map;
import java.util.List;
import java.util.Iterator;
public final class OnTV extends Enum {
public static final OnTV IDOL=
new OnTV("Idol");
public static final OnTV SURVIVOR =
new OnTV("Survivor");
public static final OnTV SEINFELD =
new OnTV("Seinfeld");
private OnTV(String show) {
super(show);
}
public static OnTV getEnum(String show){
return (OnTV) getEnum(OnTV.class, show);
}
public static Map getEnumMap() {
return getEnumMap(OnTV.class);
}
public static List getEnumList() {
return getEnumList(OnTV.class);
}
public static Iterator iterator() {
return iterator(OnTV.class);
}
}
以后我们就可以按照下面的方式使用枚举变量:OnTV.getEnum("Idol")。该调用从前面创建的枚举数据
类型返回Idol。这个例子比较简单,实际上Enum类还提供了许多有用的方法,请参见本文后面提供的
完整实例。
2.4 Collections
■ 概况:扩展了Java Collection框架,增添了新的数据结构、迭代机制和比较操作符。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:几乎所有需要操作数据结构的重要Java开发项目都可以使用Collections API。和Java
的标准实现相比,Collections API 有着诸多优势。
■ 示例应用:CollectionsDemo.java。要求CLASSPATH 中包含commons-collections.jar。
■ 说明:
要在有限的文章篇幅之内详尽地介绍Collections API 实在是太困难了,不过这里仍将涵盖大多数
最重要的类,希望能够引起你的兴趣,认真了解一下其余的类。Collections 本身的文档也提供了许多资
料并解释了每一个类的用法。
Bag 接口扩展标准的Java Collection,允许生成计数器来跟踪Bag里面的所有元素。当你想要跟踪
进出某个集合的元素的总数时,Bag 是非常有用的。由于Bag本身是一个接口,所以实际使用
的应该是实
现了该接口的类,例如HashBag 或TreeBag--从这些类的名字也可以看出,HashBag 实现的是一个
HashMap的Bag,而TreeBag实现的是TreeMap的Bag。Bag 接口中两个最重要的方法是:
getCount(Object o),
用来返回Bag 里面特定对象的出现次数;uniqueSet(),返回所有唯一元素。
Buffer接口允许按照预定义的次序删除集合中的对象,删除次序可以是LIFO(Last In First Out,
后进先出),或FIFO(First In First Out,先进先出),另外还可以是自定义的次序。下面来看看
如何实现一个Buffer,按照自然次序删除元素。
BinaryHeap 类实现了Buffer 接口,能够按照自然次序删除元素。如果要颠倒次序,则必须传入一个
false,告诉Heap 采用自然次序的逆序。
BinaryHeap heap = new BinaryHeap();
// …
// 将元素加入该Heap
heap.add(new Integer(-1));
heap.add(new Integer(-10));
heap.add(new Integer(0));
heap.add(new Integer(-3));
heap.add(new Integer(5));
//…
// 删除一个元素
heap.remove();
调用该Heap 的remove,按照自然次序,元素集合中的-10将被删除。如果我们要求按照逆序排序,
则被删除的将是5。
FastArrayList、FastHashMap 和FastTreeMap 类能够按照两种模式操作,超越了与它们对应的标准
Collection。第一种模式是"慢模式",类的修改操作(添加、删除元素)是同步的。与此相对,另一种模
式
是"快模式",对这些类的访问假定为只读操作,因此不需要同步,速度较快。在快模式中,结构性的改动
通过下列方式完成:首先克隆现有的类,修改克隆得到的类,最后用克隆得到的类替换原有的类。
FastArrayList、FastHashMap和FastTreeMap 类特别适合于那种初始化之后大部分操作都是只读操作的
多线程环境。
iterators 包为各种集合和对象提供标准Java Collection 包没有提供的迭代器。本文的示例应用示范了
ArrayIterator,通过迭代方式访问Array的内容。iterators 包里面各种迭代器的用法基本上与标准
Java 迭代器一样。
最后,comparators 包提供了一些实用的比较符。所谓比较符其实也是一个类,它定义的是如何比较
两个属于同一类的对象,决定它们的排序次序。例如,在前面提到的Buffer 类中,我们可以定义自己的
比较符,用自定义的比较符来决定元素的排序次序,而不是采用元素的自然排序次序。下面来看看具体的
实现经过。
// …
// ① 创建一个BinaryHeap 类,但这一次参数中
// 指定NullComparator。NullComparator比较
// null与其他对象,根据nullsAreHigh 标记来
// 判断null 值比其他对象大还是小:如果
// nullsAreHigh的值是false,则认为null 要比
// 其他对象小。
BinaryHeap heap2 = new BinaryHeap
(new NullComparator(false));
// …
// ② 将一些数据(包括几个null 值)加入heap:
heap2.add(null);
heap2.add(new Integer("6"));
heap2.add(new Integer("-6"));
heap2.add(null);
// …
// ③ 最后删除一个元素,Bag 包含的null 将减少
// 一个,因为null 要比其他对象小。
heap2.remove();
有关其他类Commons 组件的介绍就到这里结束。如果你想了解更多细节信息,请参见API文档,最好
再看看这些包的源代码。
三、Web类
Web 类的组件用来执行与Web 相关的任务。
3.1 FileUpload
■ 概况:一个可以直接使用的文件上载组件。
■ 官方资源:主页。由于这个组件尚未正式发布,今年二月发布的Beta版又有许多BUG,所以建议
从nightly builds 下载最新的版本。
■ 何时适用:当你想要在Java 服务器环境中加入一个易用、高性能的文件上载组件之时。
■ 示例应用:fileuploaddemo.jsp,fileuploaddemo.htm,和msg.jsp。要求服务器端应用目录的
WEB-INF/lib下面有commons-fileupload-1.0-dev.jar。
■ 说明:
FileUpload 组件解决了常见的文件上载问题。它提供了一个易用的接口来管理上载到服务器的文件,
可用于JSP和Servlet 之中。FileUpload 组件遵从RFC1867,它分析输入请求,向应用程序提供一系列上
载到服务器的文件。上载的文件可以保留在内存中,也可以放入一个临时位置(允许配置一个表示文件大
小的参数,如果上载的文件超过了该参数指定的大小,则把文件写入一个临时位置)。另外还有一些参数
可供配置,包括可接受的最大文件、临时文件的位置等。
下面介绍一下使用FileUpload 组件的步骤。
首先创建一个HTML 页面。注意,凡是要上载文件的表单都必须设置enctype属性,且属性的值必须
是multipart/form-data,同时请求方法必须是POST。下面的表单除了上载两个文件,另外还有一个普通
的文本输入框:
BR>可以看到,所有验证规则都在formset 元素之内声明。formset 元素之内首先声明要验
证的表单,表单之内列出了要验证的输入域及其验证条件。在本例中,我们希望验证
myFormBean的name属性,检查该属性是否能够通过containsStar的验证(也即name 属性
的值是否包含"*"字符)。
② 以XML文件为基础,创建一个Validator 实例并予以初始化。
// 装入验证器XML 文件
InputStream in = getClass().getResourceAsStream
("validator.xml");
// 创建一个ValidatorResources
ValidatorResources resources = new ValidatorResources();
// 初始化验证器资源
ValidatorResourcesInitializer.initialize(resources, in);
// 创建Validator
Validator validator = new Validator(resources, "myFormBean");
validator.addResource(Validator.BEAN_KEY, bean);
③ 验证Bean。验证的结果是一个ValidatorResults,其中包含了各个要求验证的属性
按照各自的验证条件执行验证的结果。
// 执行验证
ValidatorResults results = validator.validate();
④ 处理ValidationResults。
//验证结果对象ValidationResults 可能还包含了验证其他表单属性的结果,
//对于每一个属性,我们都可以单独提取其验证结果。
ValidatorResult result = results.getValidatorResult("name");
// 对于每一个属性,我们可以分别检查各个验证条件的检查结果。
// 例如,name 属性通过了containsStar 验证吗?
System.err.println("name 属性包含"*"字符的测试结果:" +
result.isValid("containsStar"));
对于每一个ValidationResult 的实例,我们可以查询它是否通过了某项特定的检查。
例如,在上面的代码中,我们用result.isValid('containsStart')表达式来检查name 属
性的ValidatorResult 实例,看看它是否通过了containsStar 验证。
对于Web 应用来说,Validator是一个相当有用的组件,它提供了一组预定义的验证器,
极大地方便了用户输入合法性的验证。预定义的验证器可以用来(但不限于)检查输入值的
范围、数据类型、长度,以及email 地址和地理位置检查。此外,我们还可以自己定义验证
器并将它加入到Validator框架之中。
中国浙江孙悟空BR>
10000BR>BR>
猪八戒BR>
10001BR>BR>BR>第二个文件是规则文件rules.xml。rules.xml 告诉Digester要在company.xml中查找什么、找到了
之后执行哪些操作:
paramcount="0" />
methodname="setAddress" paramcount="0" />
paramcount="0" />
"setEmployeeNo" paramcount="0" />
BR>BR>这个文件有哪些含义呢?第一条规则,
classname="Company" />,告诉Digester 如果遇到了模式company,则必须遵从object-create-rule,
也就是要创建一个类的实例!那么要创建的是哪一个类的实例呢?classname="Company"属性指定了类
的名称。因此,解析company.xml 的时候,当遇到顶级的company元素,等到object-create-rule规则执
行完毕,我们就拥有了一个Digester 创建的Company 类的实例。
现在要理解call-method-rule 规则也应该不那么困难了,这里call-method-rule 的功能是在遇到
company/name 或company/address 模式时调用一个方法(方法的名字通过methodname 属性指定)。
最后一个模式匹配值得注意,它把规则嵌套到了匹配模式之中。两种设定规则和模式的方式都是
Digester 接受的,我们可以根据自己的需要任意选择。在这个例子中,模式里面定义的规则在遇到
company/employee 模式时创建一个Employee 类的对象,设置其属性,最后用set-next-rule将这个雇员
加入到顶层的Company。
创建好上面两个XML 文件之后,只要用两行代码就可以调用Digester了:
Digester digester = DigesterLoader.createDigester(rules.toURL());
Company company = (Company)digester.parse(inputXMLFile);
第一行代码装入规则文件,创建一个Digester。第二行代码利用该Digester 来应用规则。请参见本文
后面提供的DigesterDemo.java 完整源代码。
2.3 Jelly
■ 概况:一种基于Java和XML 的脚本语言。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:简单地说,当你想要一种灵活的、可扩展的XML 脚本工具之时。
■ 示例应用:JellyDemo.java,jellydemo.xml以及TrivialTag.java。要求CLASSPATH 中必须有
commons-jelly-1.0-dev.jar、dom4j.jar、commons-logging.jar、commons-beanutils.jar以及
commons-collections.jar。
■ 说明:
要说清楚Jelly到底是什么以及它扮演着哪种角色是件很不容易的事情。Jelly 试图提供一个通用的
XML 脚本引擎,这种脚本引擎是可以由开发者通过定制动作和标记扩展的,XML文档之中的元素映射到
JavaBean,而XML 元素的属性映射到JavaBean的属性。从某种意义上说,Jelly是一种结合了Betwixt
和Digester的工具,但Jelly更强大,具有更好的可扩展性。
一个Jelly 系统由多个组件构成。第一个组件是Jelly 脚本,它是一种由Jelly引擎解析的XML文档,
经过解析的XML 文档元素被绑定到Jelly 标记动态处理。第二个组件是Jelly标记,它是一种实现了Jelly
的Tag 接口的JavaBean,凡是Jelly 标记都可以实现doTag 方法,这个doTag 方法就是当脚本引擎遇到
XML 文档中的特定元素时所执行的方法。Jelly正是通过这一机制实现动态的脚本处理能力,从某种意义
上看,有点类似于Digester 的工作机制。
Jelly 带有许多预定义的标记,其中部分标记提供核心Jelly支持,其他标记用来提供解析、循环、
条件执行代码等方面的支持。另外,Jelly 还为Ant任务提供了广泛的支持。
要在Java应用程序中使用Jelly,首先要创建一个JellyContext的实例,例如:JellyContext context
= new JellyContext();。我们可以把JellyContext对象看成是一个编译和运行Jelly脚本的运行环境。
有了JellyContext 就可以运行Jelly 脚本。JellyContext的输出实际上是一个XMLOutput类的实例:
context.runScript(new File("jellydemo.xml"), output);。
创建自定义标记时,我们既可以覆盖上面提到的doTag方法(如下面的例子所示),或者提供一个执
行方法,如invoke()或run():
public void doTag(XMLOutput output) throws Exception {
// 在这里加入要执行的操作,
// 例如设置属性、访问文件系统等…
this.intProp = 3;
}
下面提供了一个定义Jelly 脚本的XML 文件示例:
xmlns:tr="trivialTag">
Hello World
这个例子用到jelly:define 和jelly:core标记,以及一个trivialTag 标记。当遇到trivial标记
实例时,Jelly创建相应的JavaBean 的实例,执行doTag 方法(或者也可以是一个run 或invoke之类可
调用的方法)。
Jelly 还有许多其他功能,它既可以直接从命令行或Ant脚本运行,也可以嵌入到应用程序的代码之
内,请参见Jelly 文档了解详情。
2.4 JXPath
■ 概况:Java中的XPath 解释器。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:当你想要在JavaBean、DOM或其他对象构成的结构中应用XPath 查询之时。
■ 示例应用:JXPathDemo.java,Book.java,Author.java。要求CLASSPATH必须包含
commons-jxpath-1.1.jar。
■ 说明:
下面的说明要求读者已具备基本的XPath 知识。
XPath是一种查询XML文档的语言,JXPath将同一概念应用到了其他Java对象的查询,诸如JavaBean、
Collection、Array和Map 等。
JXPathContext是JXPath中的核心类,它利用一个工厂方法来定位和创建一个上下文的实例。由于
有了这一机制,必要时开发者可以插入一个新的JXPath 的实现。要使用JXPathContext,只要简单地向
它传递一个JavaBean、Collection 或Map,例如:JXPathContext context =
JXPathContext.newContext(book);。
利用JXPathContext 可执行许多任务。例如访问属性或嵌套属性,当然还可以设置属性:
System.err.println(context.getValue("title"));
System.err.println(context.getValue("author/authorId"));
context.setValue("author/authorId", "1001");
利用JXPath 还可以查找其他类型的对象,不过创建上下文对象的方式都一样,都是用上面介绍的静态
方法获得一个新的上下文,传入想要查询的对象。
结束语:有关包装类和XML 类的介绍就到这里结束。在下一篇也是最后一篇文章中,我们将了解工具
类的包。在这个系列文章的第一篇中,我们把Commons项目包含的组件分成了5类,介绍了Web类和其他
类。第二篇文章论及XML 类和包装类。这是最后一篇,探讨工具类的组件。注意Commons本身并不进行这
种分类,这里进行分类纯粹是为说明和组织方便起见。
第三部分、工具类
工具类包含BeanUtils、Logging、DBCP、Pool和Validator 这几个组件。
一、BeanUtils
■ 概况:提供了动态操作JavaBean 的工具。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:当你需要动态访问JavaBean,但对已编译好的accessor 和
modifier 一无所知之时。被动态访问的JavaBean 必须遵从JavaBeans
specification 定义的命名设计规范。
■ 示例应用:BeanUtilsDemo.java,AppLayer1Bean.java,
AppLayer2Bean.java,SubBean.java。要求CLASSPATH 中必须包含commons-beanutils.jar、
commons-logging.jar 以及commons-collections.jar。
■ 说明:
在动态Java应用程序设计环境中,我们不一定能够预先获知JavaBean 的各种set、get 方法。即使已经
知道了这些方法的名字,为Bean 的每个属性依次写出setXXX 或getXXX方法也是一件很麻烦的事情。考
虑一下这种情形:几个几乎完全相同的Bean 从应用的一个层传递到另一个层,你会为每一个属性调用
bean1.setXXX(bean2.getXXX())吗?虽然你可以这么做,但并非一定得这么做,因为你可以让BeanUtils
为你完成这些繁琐的操作!BeanUtils可以帮助开发者动态地创建、修改和复制JavaBean。
BeanUtils 能够操作符合下列条件的JavaBean:
⑴ JavaBean必须提供一个没有参数的构造函数。
⑵ JavaBean的属性必须能够通过getXXX和setXXX方法访问和修改。对于Boolean属性,也允许使用isXXX
和setXXX。JavaBean的属性可以是只读或只写的,也就是说,允许只提供属性的set或get方法。
⑶ 如果不采用传统的命名方式(即用get 和set),改用其它方式命名JavaBean 的accessor和modifier
,
那么必须通过与JavaBean 关联的BeanInfo 类声明这一点。
下面来看一个简单的例子。
要获取和设置JavaBean 的简单属性,分别使用PropertyUtils.
getSimpleProperty(Object bean, String name)以及PropertyUtils.
setSimpleProperty(Object bean, String name, Object value)方法。如下面的例子所示,其中
AppLayer1Bean.java和AppLayer2Bean.java 定义了两个测试用的JavaBean。
PropertyUtils.setSimpleProperty(app1Bean,"intProp1", new Integer(10));
System.err.println("App1LayerBean, stringProp1: " +
PropertyUtils.getSimpleProperty(app1Bean, "stringProp1"));
既然我们可以通过直接调用Bean 的方法(app1Bean.getStringProp1()或
app1Bean.setIntProp1(10))来获取或设置Bean 的属性,为什么还要使用setSimpleProperty、
getSimpleProperty方法呢?这是因为,我们不一定能够预先知道JavaBean 属性的名字,因此也不一定
知道要调用哪些方法才能获取/设置对应的属性。这些属性的名字可能来自其他过程或外部应用程序设置
的变量。因此,一旦搞清楚了JavaBean的属性的名字并把它保存到一个变量,你就可以将变量传递给
PropertyUtils,再也不必依靠其他开发者才能预先得知正确的方法名字。
那么,如果JavaBean 的属性不是简单数据类型,又该怎么办呢?例如,JavaBean的属性可能是一个
Collection,也可能是一个Map。在这种情况下,我们要改用PropertyUtils.getIndexedProperty或
PropertyUtils.getMappedProperty。对于集合类属性值,我们必须指定一个索引值,规定待提取或设置
的值在集合中的位置;对于Map 类属性,我们必须指定一个键,表示要提取的是哪一个值。下面是两个例
子:
PropertyUtils.setIndexedProperty(
app1Bean, "listProp1[1]", "新字符串1");
System.err.println("App1LayerBean, listProp1[1]: " +
PropertyUtils.getIndexedProperty(app1Bean,
"listProp1[1]"));
请注意,对于可索引的属性,索引值是通过方括号传递的。例如上面的例子中,我们把JavaBean
(app1Bean)的List中索引为1 的值设置成了"新字符串1",后面的一行代码又从索引1的位置提取同
一个值。还有另一种方式也可以达到同样的目标,即使用
PropertyUtils.setIndexedProperty(Object
bean, String name, int index, Object value)和PropertyUtils.getIndexedProperty(Object bean,
String name, int index)方法,在这两个方法中索引值作为方法的参数传递。对于Map类属性,也有类
似的方法,只要改用键(而不是索引)来获取或设置指定的值。
最后,Bean的属性可能也是一个Bean。那么,怎样来获取或设置那些以属性的形式从属于主Bean 的
属性Bean 呢?只要使用PropertyUtils.getNestedProperty(Object bean, String name)和
PropertyUtils.setNestedProperty(Object bean, String name, Object value)方法就可以了。下面提
供了一个例子。
// 访问和设置嵌套的属性
PropertyUtils.setNestedProperty(app1Bean, "subBean.stringProp",
"来自SubBean 的信息,通过setNestedProperty 设置。");
System.err.println(
PropertyUtils.getNestedProperty(app1Bean,"subBean.stringProp"));
通过上面的例子可以看出,从属Bean 的属性是通过一个句点符号访问的。
上述几种访问属性的方式可以结合在一起使用,嵌套深度不受限制。具体要用到的两个方法是
PropertyUtils.getProperty(Object bean, String name)和PropertyUtils.setProperty(Object bean,
String name, Object value)。例如:PropertyUtils.setProperty(app1Bean, "subBean.listProp[0]",
"属性的值");。
这个例子是把嵌套Bean 对象和可索引属性结合在一起访问。
BeanUtils经常用于动态访问Web 应用中的请求参数。实际上,正是BeanUtils触发了Struts 项目中
把请求参数动态转换成系统JavaBean 的灵感:利用代码把用户填写的表单转换成一个Map,其中参数的
名字变成Map中的键,参数的值则来自于用户在表单中输入的数据,然后由一个简单的
BeanUtils.populate调用把这些值转换成一个系统Bean。
最后,BeanUtils 提供了一个一步到位的方法把数据从一个Bean 复制到另一个Bean:
// 把app1Bean 的数据复制到app2Bean
BeanUtils.copyProperties(app2Bean, app1Bean);
BeanUtils 还有一些这里尚未提及的实用方法。不过不必担心,BeanUtils 是Commons 中文档较为完善的
组件之一,建议读者参阅BeanUtils 包的JavaDoc 文档了解其余方法的相关信息。
二、Logging
■ 概况:一个封装了许多流行日志工具的代码库,并提供统一的日志访问接口。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:当你的应用需要一种以上的日志工具之时,或者预期以后会有这种需要之时。
■ 示例应用:LoggingDemo.java,commons-logging.properties。要求CLASSPATH中必须包含
commons-logging.jar,有时还需要log4j.jar。
■ 说明:
日志(Logging)使得我们能够调试和跟踪应用程序任意时刻的行为和状态。在任何规模较大的应用中,
Logging 都是不可或缺的组成部分,因此现在已经有许多第三方Logging工具,它们免去了开发者自己编
写Logging API 之劳。实际上,即使JDK 也带有构造好了的Logging API。既然已经有这么多选择(log4j
,
JDK,Logkit,等等),通常我们总是可以找到最适合自己应用要求的现成API。
不过也有可能出现例外的情形,例如一个熟悉的Logging API 不能和当前的应用程序兼容,或者是由于某
种硬性规定,或者是由于应用的体系结构方面的原因。Commons项目Logging 组件的办法是将记录日志的
功能封装为一组标准的API,但其底层实现却可以任意修改和变换。开发者利用这个API来执行记录日志
信息的命令,由API 来决定把这些命令传递给适当的底层句柄。因此,对于开发者来说,Logging组件对
于任何具体的底层实现都是中立的。
如果你熟悉log4j,使用Commons 的Logging API 应该不会有什么问题。即使你不熟悉log4j,只要知道
使用Logging必须导入两个类、创建一个Log 的静态实例,下面显示了这部分操作的代码:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class LoggingDemo {
private static Log log = LogFactory.getLog
(LoggingDemo.class);
// ...
}
有必要详细说明一下调用LogFactory.getLog()时发生的事情。调用该函数会启动一个发现过程,即
找出必需的底层日志记录功能的实现,具体的发现过程在下面列出。注意,不管底层的日志工具是怎么找
到的,它都必须是一个实现了Log 接口的类,且必须在CLASSPATH 之中。Commons LoggingAPI直接提供
对下列底层日志记录工具的支持:Jdk14Logger,Log4JLogger,LogKitLogger,NoOpLogger(直接丢弃
所有日志信息),还有一个SimpleLog。
⑴ Commons 的Logging 首先在CLASSPATH 中寻找一个commons-logging.properties 文件。这个属性
文件至少必须定义org.apache.commons.logging.Log 属性,它的值应该是上述任意Log 接口实现的完整
限定名称。
⑵ 如果上面的步骤失败,Commons 的Logging接着检查系统属性
org.apache.commons.logging.Log。
⑶ 如果找不到org.apache.commons.logging.Log系统属性,Logging接着在CLASSPATH中寻找log4j
的类。如果找到了,Logging就假定应用要使用的是log4j。不过这时log4j 本身的属性仍要通过
log4j.properties 文件正确配置。
⑷ 如果上述查找均不能找到适当的Logging API,但应用程序正运行在JRE 1.4 或更高版本上,则
默认使用JRE 1.4 的日志记录功能。
⑸ 最后,如果上述操作都失败,则应用将使用内建的SimpleLog。SimpleLog 把所有日志信息直接输
出到System.err。
获得适当的底层日志记录工具之后,接下来就可以开始记录日志信息。作为一种标准的API,Commons
Logging API 主要的好处是在底层日志机制的基础上建立了一个抽象层,通过抽象层把调用转换成与具体
实现有关的日志记录命令。
本文提供的示例程序会输出一个提示信息,告诉你当前正在使用哪一种底层的日志工具。请试着在不
同的环境配置下运行这个程序,例如,在不指定任何属性的情况下运行这个程序,这时默认将使用
Jdk14Logger;然后指定系统属性-Jorg.apache.commons.logging.Log=org.apache.commons.logging.imp
l.SimpleLog
再运行程序,这时日志记录工具将是SimpleLog;最后,把Log4J的类放入
CLASSPATH,只要正确设置了log4j 的log4j.properties 配置文件,就可以得到Log4JLogger输出的信息
。
三、Pool
■ 概况:用来管理对象池的代码库。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:当你需要管理一个对象实例池之时。
■ 示例应用:PoolDemo.java 和MyObjectFactory.java。要求CLASSPATH中必须有commons-pool.jar
和commons-collections.jar。
■ 说明:
Pool 组件定义了一组用于对象池的接口,另外还提供了几个通用的对象池实现,以及
一些帮助开发者自己创建对象池的基础类。
对于大多数开发者来说,对象池应该不算什么新概念了。也许许多读者已经在访问数据
库的时候使用过数据库连接池,对象池的概念其实也相似。对象池允许开发者在缓冲区中创
建一组对象(创建对象的操作可以通过应用的配置文件完成,或者也可以在应用的启动阶段
完成),当应用程序需要用到对象时就可以很快获得相响应。如果应用程序不再需要对象,
它仍旧把对象返回给缓冲池,下次需要使用对象时再从缓冲池提取。
Pool 组件允许我们创建对象(实例)池,但不限制我们一定要使用某个具体的实现。
Pool 组件本身提供了几种实现,必要时我们还可以创建自己的实现。
Pool 组件包含三个基本的类:ObjectPool,这是一个定义和维护对象池的接口;
ObjectPoolFactory,负责创建ObjectPool 的实例;还有一个PoolableObjectFacotry,它
为那些用于ObjectPool 之内的实例定义了一组生命周期方法。
如前面指出的,Pool组件包含几种通用的实现,其中一个就是GenericObjectPool,下
面通过一个实例来看看它的用法。
① 创建一个PoolableObjectFactory。这个工厂类定义对象如何被创建、拆除和验证。
import org.apache.commons.pool.PoolableObjectFactory;
public class MyObjectFactory implements
PoolableObjectFactory {
private static int counter;
// 返回一个新的字符串
public Object makeObject() {
return String.valueOf(counter++);
}
public void destroyObject(Object obj) {}
public boolean validateObject(Object obj)
{ return true; }
public void activateObject(Object obj) {}
public void passivateObject(Object obj) {}
}
本例创建了一个序号不断增加的String 对象的池,验证操作(validateObject)总是
返回true。
② 利用PoolableObjectFactory 创建一个GenericObjectPool,maxActive、maxIdle
等选项都采用默认值。
GenericObjectPool pool = new GenericObjectPool
(new MyObjectFactory());
③ 从对象池"借用"一个对象。
System.err.println("Borrowed: " + pool.borrowObject());
④ 把对象返回给对象池。
pool.returnObject("0");
对象池的状态可以通过多种方法获知,例如:
// 有多少对象已经激活(已被借用)?
System.err.println("当前活动的对象数量: " + pool.getNumActive());
本文后面提供的PoolDemo.java 提供了完整的源代码。
四、DBCP
■ 概况:数据库连接池。建立在Pool 组件的基础上。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:需要访问关系数据库之时。
■ 示例应用:DBCPDemo.java。要求CLASSPATH中必须有commons-dbcp.jar、
commons-pool.jar 以及commons-collections.jar。另外还要能够访问数据库,配置适合该
数据库的JDBC 驱动程序。示例应用测试的是一个MySQL数据库连接,驱动程序是MySQL JDBC
driver。注意运行这个程序需要二进制文件的nightly 版,当前的正式发行版缺少某些必需的类。
最后,运行这个示例程序时,应当确保已经为JDBC
驱动程序设置了系统属性(-Djdbc.drivers=com.mysql.jdbc.Driver)。
■ 说明:
DBCP 建立在Pool组件的基础上,提供了数据库连接缓冲池机制。与常规的连接池相比,
DBCP 的使用要稍微复杂一点,因为它的思路是以伪JDBC 驱动程序的形式提供一个通用的体
系。不过,前面我们已经了解了Pool组件的基本知识,现在要理解DBCP 的用法应该也很简单了。
// ...
// ① 创建一个GenericObjectPool 类的实例。
GenericObjectPool pool = new GenericObjectPool(null);
// ...
// ② 在前面讨论Pool 组件时提到GenericObjectPool
// 要求有一个PoolableObjectFactory 来创建需
// 要缓冲的Object 的实例,对于DBCP 来说,
// 这一功能现在由PoolableConnectionFactory提
// 供,如下面的例子所示:
DriverManagerConnectionFactory cf =
new DriverManagerConnectionFactory(
"jdbc:mysql://host/db", "username", "password");
PoolableConnectionFactory pcf = new PoolableConnectionFactory(
CF, pool, null, "SELECT * FROM mysql.db", false, true);
// ...
// ③ 现在,我们只要创建并注册PoolingDriver:
new PoolingDriver().registerPool("myPool", pool);
接下来就可以从这个连接池提取连接了。注意创建这个连接池时采用了maxActive、
maxIdle 等选项的默认值,如有必要,你可以在前面步骤1创建GenericObjectPool 类的实
例时自定义这些值。DBCPDemo.java 提供了一个完整的实例。
五、Validator
■ 概况:一个收集了常见用户输入验证功能的API。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:对JavaBean 执行常规验证操作之时。
■ 示例应用:ValidatorDemo.java,MyValidator.java,MyFormBean.java,
validation.xml。要求CLASSPATH 中必须有commons-validator.jar,
commons-beanutils.jar,commons-collections.jar,commons-digester.jar,以及
commons-logging.jar。
■ 说明:
如果你曾经用Struts 开发过Web 应用,那么应该已经用过Validator包了。Validator
包极大地简化了用户输入数据的检验。不过,Validator 并不局限于Web应用,它还可以方
便地用于其它使用了JavaBean的场合。
Validator 允许为用户输入域定义验证条件,支持错误信息国际化,允许创建自定义的
验证器,此外,Validator 包还提供了一些预定义的可以直接使用的验证器。
验证规则和验证方法用XML文件定义(可以用一个或者多个XML 文件定义,但通常而言,
把它们分开比较好)。验证方法文件定义了要用到的验证器,指定各个实际实现验证器的
Java 类(不要求这些类实现某些特定的接口,也不要求这些类必须从特定的类派生,只需
要遵从方法定义文件中声明的定义就可以了)。
下面我们就来构造一个自定义的验证器,它的功能是检查Bean的一个String属性是否
包含特定的字符("*")。
import org.apache.commons.validator.*;
public class MyValidator {
public static boolean validateContainsChar(
Object bean, Field field) {
// 首先获得Bean 的属性(即一个String 值)
String val = ValidatorUtil.getValueAsString
(bean, field.getProperty());
// 根据属性中是否包含"*"字符,返回true 或false。
return ((val.indexOf('*') == -1)?false:true);
}
}
ValidatorUtil类提供了许多实用方法,例如ValidatorUtil.getValueAsString用来
提取Bean的属性值并返回一个String。现在我们要在XML文件中声明MyValidator验证器。
classname="MyValidator"
method="validateContainsChar"
methodParams="java.lang.Object,
org.apache.commons.validator.Field" />
BR>可以看到,XML 文件详细地定义了验证方法的特征,包括该方法的输入参数。下面来看
看使用这个验证器的步骤。
① 在上面的XML文件中加入我们要实现的验证规则。