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(); } } |
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,创建GetMethod实例时在参数中指定我们想要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和库的兴趣。
请从这里下载本文代码:JakartaCommons1_code.zip
* *
*******************************************************************************************
在上一篇文章中,我们将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类的介绍就到这里结束。在下一篇也是最后一篇文章中,我们将了解工具类的包。
请从这里下载本文代码:jakartacommons2code.zip。
*******************************************************************************************
* *
*******************************************************************************************
在这个系列文章的第一篇中,我们把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 Logging API直接提供对下列底层日志记录工具的支持: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.impl.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 框架之中。