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类,如下表所示。
![](http://developer.ccidnet.com/pub/attachment/2003/8/247208.gif)
必须指出的是,这种分类只是为了方便文章说明,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和库的兴趣。