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。下面的表单除了上载两个文件,另外还有一个普通
的文本输入框:
<form name="myform" action="fileuploaddemo.jsp"
method="post" enctype="multipart/form-data">
输入你的名字:<br />
<input type="text" name="name" size="15"/><br />
图形:<br />
<input type="file" name="myimage"><br/>
文件:<br />
<input type="file" name="myfile"><br /><br />
<input type="submit" name="Submit"
value="Submit your files"/>
接下来创建JSP页面。
// …
// ① 检查输入请求是否为multipart的表单数据。
boolean isMultipart = FileUpload.
isMultipartContent(request);
// …
// ② 为该请求创建一个句柄,通过它来解析请求。执行
// 解析后,所有的表单项目都保存在一个List中。
DiskFileUpload upload = new DiskFileUpload();
// 通过句柄解析请求,解析得到的项目保存在一个List 中
List items = upload.parseRequest(request);
// …
// ③ 通过循环依次获得List里面的文件项目。要区分表示
// 文件的项目和普通的表单输入项目,使用isFormField()
// 方法。根据处理请求的要求,我们可以保存上载的文
// 件,或者一个字节一个字节地处理文件内容,或者打
// 开文件的输入流。
Iterator itr = items.iterator();
while(itr.hasNext()) {
FileItem item = (FileItem) itr.next();
// 检查当前的项目是普通的表单元素,还是一个上载的文件
if(item.isFormField()) {
// 获得表单域的名字
String fieldName = item.getFieldName();
// 如果表单域的名字是name…
if(fieldName.equals("name"))
request.setAttribute("msg",
"Thank You: " + item.getString());
} else {
// 该项目是一个上载的文件,把它保存到磁盘。
// 注意item.getName()
// 会返回上载文件在客户端的完整路径名称,这似乎是一个BUG。
// 为解决这个问题,这里使用了fullFile.getName()。
File fullFile = new File(item.getName());
File savedFile = new File
(getServletContext().getRealPath("/"),
fullFile.getName());
item.write(savedFile);
}
}
我们可以通过上载句柄的upload.setSizeMax 来限制上载文件的大小。当上载文件的大小超过允许的
值时,程序将遇到异常。在上面的例子中,文件大小的限制值是-1,表示允许上载任意大小的文件。
还有其他一些略有变化的使用形式,正如前面所指出的,我们可以在上载的文件上打开一个输入流,
或者让它们驻留在内存中直至空间占用达到一定的限制值,或者在判断文件类型的基础上,以String 或
Byte 数组的形式获取其内容,或者直接删除文件。这一切都只要使用FileItem 类提供的方法就可以方便
地做到(DefaultFileItem 是FileItem的一个实现)。
3.2 HttpClient
■ 概况:这个API 扩展了java.net包,提供了模拟浏览器的功能。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:当你要构造Web 浏览器的功能;当你的应用需要一种高效的办法进行HTTP/HTTPS通信时。
■ 示例应用:HttpClientDemo.java。要求CLASSPATH中有commons-httpclient.jar,
common-logging.jar。要求使用JDK 1.4 或更高版本。
■ 说明:
HttpClient 扩展和增强了标准java.net 包,是一个内容广泛的代码库,功能极其丰富,能够构造出
各种使用HTTP 协议的分布式应用,或者也可以嵌入到现有应用,为应用增加访问HTTP 协议的能力。
在Commons 稳定版中,HttpClient 的文档似乎要比其他包更完善一些,而且还带有几个实例。下面我们
通过
一个简单的例子来了解如何提取一个Web 页面,HttpClient 文档中也有一个类似的例子,我们将扩充那
个例子使其支持SSL。注意本例需要JDK 1.4 支持,因为它要用到Java Secure Socket Connection库,
而这个库只有JDK 1.4 及更高的版本才提供。
① 首先确定一个可以通过HTTPS 下载的页面,本例使用的是https://www.paypal.com/。同时确保
%JAVA_HOME%/jre/lib/security/java.security文件包含了下面这行代码:
security.provider.2=com.sun.net.ssl.internal.ssl.Provider。
除了这些设置之外,HTTPS连接的处理方式没有其他特别的地方--至少对于本例来说如此。不过,如
果远程网站使用的根证书不被你使用的Java 认可,则首先必须导入它的证书。
② 创建一个HttpClient的实例。HttpClient 类可以看成是应用的主驱动程序,所有针对网络的功
能都依赖于它。HttpClient 类需要一个Connection Manager来管理连接。
HttpConnectionManager允许
我们创建自己的连接管理器,或者,我们也可以直接使用内建的SimpleHttpConnectionManager或
MultiThreadedHttpConnectionManager类。如果在创建HttpClient 时没有指定连接管理器,HttpClient
默认使用SimpleHttpConnectionManager。
// 创建一个HttpClient 的实例
HttpClient client = new HttpClient();
③ 创建一个HttpMethod的实例,即确定与远程服务器的通信要采用哪种传输方式,HTTP 允许采用
的传输方式包括:GET,POST,PUT,DELETE,HEAD,OPTIONS,以及TRACE。这些传输方式分别作为一个
独立的类实现,但所有这些类都实现HttpMethod接口。在本例中,我们使用的是GetMethod,创建GetMeth
od
实例时在参数中指定我们想要GET 的URL。
// 创建一个HttpMethod 的实例
HttpMethod method = new GetMethod(url);
④ 执行HttpMethod 定义的提取操作。执行完毕后,executeMethod方法将返回远程服务器报告的状态
代码。注意executeMethod属于HttpClient,而不是HttpMethod。
// 执行HttpMethod定义的提取操作
statusCode = client.executeMethod(method);
⑤ 读取服务器返回的应答。如果前面的连接操作失败,程序将遇到HttpException或IOException,
其中IOException 一般意味着网络出错,继续尝试也不太可能获得成功。服务器返回的应答可以按照多种
方式读取,例如作为一个字节数组,作为一个输入流,或者作为一个String。获得服务器返回的应答后,
我们就可以按照自己的需要任意处置它了。
byte[] responseBody = method.getResponseBody();
⑥ 最后要做的就是释放连接。
method.releaseConnection();
以上只是非常简单地介绍了一下HttpClient 库,HttpClient 实际的功能要比本文介绍的丰富得多,
不仅健壮而且高效,请参阅API 文档了解详情。
3.3 Net
■ 概况:一个用于操作Internet基础协议的底层API。
■ 官方资源:主页,二进制,源代码。
■
何时适用:当你想要访问各种Internet底层协议之时(Finger,Whois,TFTP,Telnet,POP3,FTP,NNTP
,以及SMTP)。
■ 示例应用:NetDemo.java。要求CLASSPATH中包含commons-net-1.0.0.jar。
■ 说明:
Net 包是一个强大、专业的类库,类库里的类最初属于一个叫做NetComponents 的商业产品。
Net 包不仅支持对各种低层次协议的访问,而且还提供了一个高层的抽象。大多数情况下,Net包提
供的抽象已能满足一般需要,它使得开发者不再需要直接面对各种协议的Socket 级的低层命令。使用高
层抽象并不减少任何功能,Net API 在这方面做得很出色,既提供了足够的功能,又不至于在特色方面作
过多的妥协。
SocketClient 是支持所有协议的基础类,它是一个抽象类,聚合了各种协议都需要的公用功能。各种
不同协议的使用过程其实很相似,首先利用connect方法建立一个指向远程服务器的连接,执行必要的
操作,最后终止与服务器的连接。下面通过实例介绍具体的使用步骤。
// …
// ① 创建一个客户端。我们将用NNTPClient
// 从新闻服务器下载新闻组清单。
client = new NNTPClient();
// …
// ② 利用前面创建的客户端连接到新闻服务器。
// 这里选用的是一个新闻组较少的服务器。
client.connect("aurelia.deine.net");
// …
// ③ 提取新闻组清单。下面的命令将返回一个
// NewsGroupInfo 对象的数组。如果指定的服
// 务器上不包含新闻组,返回的数组将是空的,
// 如果遇到了错误,则返回值是null。
list = client.listNewsgroups();
//...
// ④ 最后终止与服务器的连接。
if (client.isConnected())
client.disconnect();
必须说明的是,listNewsgroups命令可能需要较长的时间才能返回,一方面是因为网络速度的影响,
另外也可能是由于新闻组清单往往是很庞大的。NewsGroupInfo对象包含有关新闻组的详细信息,并提供
了一些操作新闻组的命令,比如提取文章总数、最后发布的文章、发布文章的权限,等等。
其他客户端,例如FingerClient、POP3Client、TelnetClient 等,用法也差不多。
结束语:有关Web相关类和其他类的介绍就到此结束。在下一篇文章中,我们将探讨XML类和包装类,
最后一篇文章则介绍工具类。
希望读者有兴趣试试本文提供的程序实例。很多时候Jakarta Commons 给人以混乱的感觉,希望本文
使你加深了对Jakarta Commons 了解,或者至少引起了你对Commons 子项目以及它提供的各种实用API 和
库的兴趣。
第二部分XML 类和包装类
上一篇文章中,我们将Jakarta Commons的组件分成了五类,并介绍了其中的Web类和其他类,本文接着
介绍XML 类和包装类,接下来的最后一篇文章将介绍工具类。注意Commons本身并不进行这种分类,这里
进行分类纯粹是为组织方便起见。
一、包装类
这一类包含Codec 和Modeler 两个组件。
1.1 Codec
■ 概况:提供常用的编码器和解码器。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:当你需要Base64 和Hex编码功能的标准实现之时。
■ 示例应用:CodecDemo.java。要求CLASSPATH必须包含commons-codec-1.1.jar。
■ 说明:
Codec 里面的类分成两个包,其中一个包实现的是常用的Base64 和Hex 编码机制,另一个包是语言、
语音方面的编码。两个包的用法相似,鉴于语言、语音的编码并不是很常用,所以下面主要介绍第一个包
。
Base64编码主要用于Email 传输。定义MIME 文档传输的RFC 规定了Base 64 编码,从而使得任何二进制
数据都可以转换成可打印的ASCII字符集安全地传输。例如,假设要通过Email 传输一个图形文件,
Email 客户端软件就会利用Base64 编码把图形文件的二进制数据转换成ASCII 码。在Base64编码中,
每三个8 位的字节被编码成一个4 个字符的组,每个字符包含原来24 位中的6 位,编码后的字符串大小
是
原来的1.3倍,文件的末尾追加"="符号。除了MIME文档之外,Base64 编码技术还用于BASIC认证机制
中HTTP 认证头的"用户:密码"字符串。
Base64类的使用相当简单,最主要的两个静态方法是:Base64.encodeBase64(byte[] byteArray),
用于对字节数组中指定的内容执行Base64 编码;Base64.decodeBase64(byte[] byteArray),用于对字节
数组中指定的内容执行Base64解码。另外,Base64还有一个静态方法
Base64.isArrayByteBase64(byte[]
byteArray),用于检测指定的字节数组是否可通过Base64 测试(即是否包含了经过Base64编码的数据,
如前所述,Base64 编码的结果只包含可打印的ASCII字符)。
byte[] encodedBytes=Base64.encodeBase64(testString.getBytes());
String decodedString=new String(Base64.decodeBase64(encodedBytes));
System.err.println("/'^/'是一个合法的Base64 字符吗?"
+ Base64.isArrayByteBase64(invalidBytes));
Hex 编码/解码就是执行字节数据和等价的十六进制表示形式之间的转换。Hex 编码的编码、解码过程
和Base64 相似,此处不再赘述。
1.2 Modeler
■ 概况:根据JMX(Java Management Extensions)规范的定义,支持对Model MBean(Managed Bean)
的配置和实例化。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:当你想要创建和管理Model MBean,以便利用标准的管理API来管理应用之时。
■ 示例应用:ModelerDemo.java,DemoManagedBean.java和mbeans-descriptors.xml。要求
CLASSPATH 中包含commons-modeler-1.0.jar、commons-logging.jar、
commons-digester.jar、
commons-collections.jar、commons-beanutils.jar,以及Sun的JMX参考实现jmxri.jar。
■ 说明:
下面的说明要求读者对JMX 有一定的了解。
Managed Bean 简称MBean,是一种关联到应用程序中被管理组件的Bean,是一种对资源抽象。Model
MBean 是一种特殊的MBean,具有高度动态和可配置的特点,但Model MBean 的这种能力是有代价的,
程序员需要设置大量的元信息来告诉JMX如何创建Model MBean,这些元信息包括组件的属性、操作和其它
信息。Modeler 的目的就是降低程序员实现Model MBean 的工作量,它提供的一组函数为处理元数据信息
带来了方便。另外,Modeler还提供了注册工具和一个基本的Model MBean。
Modeler 允许以XML文件的形式定义元数据信息,该XML文件应当遵从随同Modeler 提供的DTD 定义。
元数据信息用来在运行时创建注册信息,注册信息是所有Model MBean 的中心知识库,实际上相当于一个
创建这类Bean 的工厂。
下面我们首先为一个Managed Bean(DemoManagedBean)创建这个XML文件。DemoManagedBean有一
个name 属性,可读写。
<?xml version="1.0" encoding="GB2312" ?>
<!DOCTYPE mbeans-descriptors PUBLIC
"-//Apache Software Foundation
//DTD Model MBeans Configuration File"
"http://jakarta.apache.org/commons/dtds/mbeans-descriptors.dtd">
<!-- JMX MBean 的描述-->
<mbeans-descriptors>
<mbean name="ManagedBean" description="Example Managed Bean"
type="ManagedBean">
<attribute name="name" description="Simple Name"
type="java.lang.String" />
<constructor name="ManagedBean"/>
</mbean>
</mbeans-descriptors>
可以看到,这个XML 文件提供了许多ManagedBean 的信息,包括它的属性、构造函数,另外还有它的
操作(不过本例没有显示),这就是所谓的元数据信息。如果你打算扩展随同Modeler 提供的标准MBean
(称为BaseModelMBean),可以在mbean元素中以属性的形式指定Model MBean的类名称:。在前面的
例子中,标准的Model MBean只是简单地把所有调用直接传递给ManagedBean 类。
接下来,我们要注册上述信息。注意通过描述文件装入注册信息之后,我们通过一个静态方法提取格
式化的注册信息:
// 创建一个Registry
Registry registry = null;
try {
URL url = ModelerDemo.class.getResource
("mbeans-descriptors.xml");
InputStream stream = url.openStream();
Registry.loadRegistry(stream);
stream.close();
registry = Registry.getRegistry();
} catch (Throwable t) {
t.printStackTrace(System.out);
System.exit(1);
}
创建好Registry之后,我们要创建一个Model MBean,并将它注册到默认的管理服务器。这样,任何
JMX 客户程序都可以通过Model MBean 调用Managed Bean 的功能了。
// 获得一个Managed Bean 实例的句柄
DemoManagedBean mBean = new DemoManagedBean();
// 创建一个Model MBean,并将它注册到MBean服务器
MBeanServer mServer = registry.getServer();
ManagedBean managed = registry.findManagedBean("ManagedBean");
try {
ModelMBean modelMBean = managed.createMBean(mBean);
String domain = mServer.getDefaultDomain();
ObjectName oName = new ObjectName(domain +
":type=ManagedBean");
mServer.registerMBean(modelMBean, oName);
} catch(Exception e) {
System.err.println(e);
System.exit(0);
}
try {
ObjectName name =
new ObjectName(mServer.getDefaultDomain() +
":type=ManagedBean");
ModelMBeanInfo info = (ModelMBeanInfo) mServer.
getMBeanInfo(name);
System.err.println(" className="+info.getClassName());
System.err.println(" description="+info.getDescription());
System.err.println(" mbeanDescriptor="+info.getMBeanDescriptor());
System.err.println("==== 测试====");
System.err.println("Name 的原始值: " +
mServer.getAttribute(name, "name"));
mServer.setAttribute(name, new Attribute("name", "Vikram"));
System.err.println("Name 的新值: " +
mServer.getAttribute(name, "name"));
} catch(Exception e) {
System.err.println(e);
System.exit(0);
}
虽然这个例子比较简单,但它仍旧清楚地说明了使用Modeler带来的方便,不妨将它与不使用Modeler
的情况下创建一个类似的Model MBean相比较。通过XML文件来描述ModelMBeanInfo不仅灵活方便,而
且也很容易扩展,比手工编写这类信息改进不少。
二、XML类
XML 类包含了与Java、XML技术相关的类,包括:Betwixt,Digester,Jelly,和JXPath。
2.1 Betwixt
■ 概况:实现XML 和JavaBean 的映射。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:当你想要以灵活的方式实现XML和Bean 的映射,需要一个数据绑定框架之时。
■示例应用:BetwixtDemo.java,Mortgage.java,mortgage.xml。要求CLASSPATH 中必须包含
commons-betwixt-1.0-alpha-1.jar、commons-logging.jar、commons-beanutils.jar、
commons-collections.jar、以及commons-digester.jar。
■ 说明:
如果你以前曾经用Castor绑定数据,一定会欣赏Betwixt的灵活性。Castor 适合在一个预定义模式
(Schema)的基础上执行Bean和XML 之间的转换;但如果你只想执行数据和XML之间的转换,最好的选
择就是Betwixt。Betwixt 的特点就是灵活,能够方便地将数据输出成为人类可阅读的XML。
Betwixt的用法相当简单。如果要把Bean 转换成XML,首先创建一个BeanWriter 的实例,设置其属性,
然后输出;如果要把XML 转换成Bean,首先创建一个BeanReader的实例,设置其属性,然后用Digester
执行转换。
将Bean转换成XML:
// 用Betwixt 将Bean转换成XML 必须有BeanWriter的实例。
// 由于BeanWriter的构造函数要求有一个写入器对象,
// 所以我们从创建一个StringWriter开始
StringWriter outputWriter = new StringWriter();
// 注意输出结果并不是格式良好的,所以需要在开始位置
// 写入下面的内容:
outputWriter.write("<?xml version='1.0' ?>");
// 创建一个BeanWriter
BeanWriter writer = new BeanWriter(outputWriter);
// 我们可以设置该写入器的各种属性。
// 下面的第一行禁止写入ID,
// 第二行允许格式化输出
writer.setWriteIDs(false);
writer.enablePrettyPrint();
// 创建一个Bean 并将其输出
Mortgage mortgage = new Mortgage(6.5f, 25);
// 将输出结果写入输出设备
try {
writer.write("mortgage", mortgage);
System.err.println(outputWriter.toString());
} catch(Exception e) {
System.err.println(e);
}
将XML 转换成Bean:
// 用Betwixt 来读取XML 数据并以此为基础创建
// Bean,必须用到BeanReader 类。注意BeanReader 类扩展了
// Digester包的Digester 类。
BeanReader reader = new BeanReader();
// 注册类
try {
reader.registerBeanClass(Mortgage.class);
// 并解析它…
Mortgage mortgageConverted =
(Mortgage)reader.parse(new File("mortgage.xml"));
// 检查转换得到的mortgage 是否包含文件中的值
System.err.println("Rate: " + mortgageConverted.getRate() +
", Years: " + mortgageConverted.getYears());
} catch(Exception ee) {
ee.printStackTrace();
}
注意,通过BeanReader 注册类时,如果顶层元素的名称和类的名称不同,必须用另一个方法注册并
指定准确的路径,如reader.registerBeanClass("toplevelelementname", Mortgage.class)。
2.2 Digester
■ 概况:提供友好的、事件驱动的高级XML 文档处理API。
■ 官方资源:主页,二进制,源代码。
■ 何时适用:当你想要处理XML 文档,而且希望能够根据XML 文档中特定的模式所触发的一组规则
来执行某些操作时。
■ 示例应用:DigesterDemo.java、Employee.java、Company.java、rules.xml以及company.xml。
要求CLASSPATH 中必须包含commons-digester.jar、commons-logging.jar、
commons-beanutils.jar
以及commons-collections.jar。
■ 说明:
Digester 在解析配置文件的时候最为有用。实际上,Digester最初就是为读取Struts 配置文件而开
发的,后来才移到Commons 包。
Digester是一个强大的模式匹配工具,允许开发者在一个比SAX 或DOM API更高的层次上处理XML
文档,当找到特定的模式(或找不到模式)时能够触发一组规则。使用Digester 的基本思路是:首先创
建一个Digester 的实例,然后用它注册一系列模式和规则,最后将XML文档传递给它。此后,Digester
就会分析XML 文档,按照注册次序来触发规则。如果XML文档中的某个元素匹配一条以上的规则,所有
的规则会按照注册次序被依次触发。
Digester本身带有12条预定义的规则。当XML文档中找到一个特定的模式时,想要调用某个方法吗?
很简单,使用预定义的CallMethodRule!另外,你不一定要使用预定的规则,Digester 允许用户通过扩
展Rule 类定义自己的规则。
在指定模式时,元素必须用绝对名称给出。例如,根元素直接用名称指定,下一层元素则通过"/"符
号引出。例如,假设company是根元素,company/employee 就是匹配其中一个子元素的模式。
Digester允许使用通配符,例如*/employee 将匹配XML 文档内出现的所有employee元素。
找到匹配的模式时,关联到该匹配模式的规则内有四个回调方法会被调用,它们是:begin,end,body,
和finish。这些方法被调用的时刻正如其名字所示,例如调用begin 和end 的时刻分别是遇到元素的开
始标记和结束标记之时,body是在遇到了匹配模式之内的文本时被调用,finish 则是在全部对匹配模式
的处理工作结束后被调用。
最后,模式可以在一个外部的规则XML 文档内指定(利用digester-rules.dtd),或者在代码之内
指定,下面要使用的是第一种办法,因为这种办法比较常用。
使用Digester 之前要创建两个XML文档。第一个就是数据或配置文件,也就是我们准备对其应用规
则的文件。下面是一个例子(company.xml)
<?xml version="1.0" encoding="gb2312"?>
<company>
<name>我的公司</name>
<address>中国浙江</address>
<employee>
<name>孙悟空</name>
<employeeNo>10000</employeeNo>
</employee>
<employee>
<name>猪八戒</name>
<employeeNo>10001</employeeNo>
</employee>
</company>
第二个文件是规则文件rules.xml。rules.xml 告诉Digester要在company.xml中查找什么、找到了
之后执行哪些操作:
<?xml version="1.0" encoding="gb2312"?>
<digester-rules>
<!-- 创建顶层的Company对象-->
<object-create-rule pattern="company" classname="Company" />
<call-method-rule pattern="company/name" methodname="setName"
paramcount="0" />
<call-method-rule pattern="company/address"
methodname="setAddress" paramcount="0" />
<pattern value="company/employee">
<object-create-rule classname="Employee" />
<call-method-rule pattern="name" methodname="setName"
paramcount="0" />
<call-method-rule pattern="employeeNo" methodname=
"setEmployeeNo" paramcount="0" />
<set-next-rule methodname="addEmployee" />
</pattern>
</digester-rules>
这个文件有哪些含义呢?第一条规则,<object-create-rule pattern="company"
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 文件示例:
<j:jelly xmlns:j="jelly:core" xmlns:define="jelly:define"
xmlns:tr="trivialTag">
<define:taglib uri="trivialTag">
<define:jellybean name="trivial" className="TrivialTag" />
</define:taglib>
<tr:trivial intProp="1" stringProp="ball">Hello World</tr:trivial>
</j:jelly>
这个例子用到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验证器。
<!-- 定义验证器方法-->
<global>
<validator name="containsStar"
classname="MyValidator"
method="validateContainsChar"
methodParams="java.lang.Object,
org.apache.commons.validator.Field" />
</global>
可以看到,XML 文件详细地定义了验证方法的特征,包括该方法的输入参数。下面来看
看使用这个验证器的步骤。
① 在上面的XML文件中加入我们要实现的验证规则。
<!-- 定义验证规则-->
<formset>
<!-- 检查Bean的name 属性是否能够通过
containsStar测试-->
<form name="myFormBean">
<field property="name" depends="containsStar">
<arg0 key="myFormBean.name" />
</field>
</form>
</formset>
可以看到,所有验证规则都在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框架之中。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=627792