如果你不熟悉Jakarta
Commons话,那么很有可能你已经重新发明了好几个轮子。在你编写更多的普通的框架或工具之前,体验一下Commons吧。它将会大大地节约你的时
间。太多的人自己写一个,其实是与Commons
Lang中的StringUtils重复的StringUtils类,或者,开发者不知道从Commons
Collections中重新创建工具,哪怕commons-collections.jar已经在classpath中可用了。
真的,
请停一下。看看Commons Collections
API,然后再回到你的任务中;我发誓你会发现一些简单有用的东西可以帮你在明年节省一周的时间。如果大家花一点时间看看Jakarta
Commons,我们将会得到更少的重复代码—我们将在重用的宗旨下真正做一些有用的事情。
我确实看到这样的情况发生过:一些人研究了一
下Commons BeanUtils或者Commons
Collections,然后总是有“啊,如果我那时知道这个的话,我就不会写那一万行的代码了”这样的时刻。Jakarta
Commons仍有一部分保持相当的神秘;比如,许多人还没有听说过Commons CLI和Commons
Configuration,并且大多数人还没有注意到Commons
Collections中的functors(算子)包的价值。在这一系列中,我会专门强调一些Jakarta
Commons中较少得到重视的工具和功能。
在这一系列的第一部分,我将探索定义在Commons
Digester中的XML规则,Commons Collections中的功能,和使用一个有趣的应用,Commons
JXPath,来查询一个对象的List。Jakarta
Commons包含的功能目的在于帮助你解决低层次的编程问题:遍历集合,解析XML和从List中检出对象。我建议你花一些时间在这些小功能上,学习
Jakarta Commons真的会为你节省不少时间。
并不简单地是学习使用Commons
Digester来解析XML或者使用CollectionUtils的Predicate来过滤一个集合,而是当你一旦意识到如何将这些功能组合起来使
用并且如何将Commons集成到你的项目中去的时候,你才会真正地看到它的好处。如果你这样做地话,你将会把commons-lang.jar,
commons-beanutils.jar,和 commons-digester.jar当成JVM本身来看待。
如果你对
Jakarta Commons更深的内容感兴趣的话,可以看一下Jakarta Commons
Cookbook。这本书给你很多方法来更好的使用Commons,并告诉你如何将Jakarta
Commons与其它的小的开源组件集成,如Velocity, FreeMarker, Lucene, 和 Jakarta
Slide。这本书,我介绍了一组广泛的工具从Commons Lang中的简单工具到组合了Commons Digester, Commons
Collections, 和Jakarta Lucene来搜索威廉.莎士比亚的著作。我希望这一系列和Jakarta Commons
Cookbook这本书能够提供给你一些有趣的低层次的编程问题的解决方案。
1. 用于Commons Digester的基于XML的规则集
Commons
Digester
1.6提供了将XML转化为对象的最简单的方法。Digester已经由O'Reilly网站上的两篇文章介绍过了:“学习和使用Jakarta
Digester”,作者是Philipp K. Janert,和“使用Jakarta Commons, 第二部分”,作者是Vikram
Goyal。两篇文章都演示了XML规则集的使用,但如何在XML中定义规则集并没有理解。大多所见到的Digester的使用是程序化地定义规则集,以
已编译的形式。你应该避免硬编码的Digester规则,特别是当你可以将映射信息存储在外部文件中或一个类路径资源中时。外部化一个Digester规
则可以更好地适应一个演化中的XML文档结构或者说一个演化中的对象模型。
为了演示在XML中定义规则集与硬编码的规则集之间的区别,考虑系统解析XML给一个Person bean,包括在下面定义的属性—id, name和age。
package org.test;
public class Person {
public String id;
public String name;
public int age;
public Person() {}
public String getId() { return id; }
public void setId(String id) {
this.id = id;
}
public String getName() { return name; }
public void setName(String name) {
this.name = name;
}
public int getAge() { return age; }
public void setAge(int age) {
this.age = age;
}
}
确认你的应用需要解析一个包含了多个person元素的XML文件。下面的XML文件,data.xml,包含了两个person元素,你想要把它们解析到Person对象中:
Tom Higgins
25
Barney Smith
75
Susan Shields
53
你
希望如果结构和XML文件的内容在未来几个月中变化,你不需要在已编译的Java代码中硬编码XML文件的结构。为了做到这一点,你需要在一个XML文件
中定义Digester的规则,并且它可以作为一种资源从类路径中装入。下面的XML文档,person-rules.xml,映射person元素到
Person bean:
paramtype="java.lang.Object"/>
上述所做的是指示Digester创建一个新的Person实例,当它遇到一个person元素时,调用add()来将Person对象加入到一个ArrayList中,设置person元素中相匹配的属性,并从下一级元素name和age中设置name和age的属性。
现
在你已经看到了Person类,会被解析的文档,和以XML的形式定义的Digester规则。现在你需要创建一个由person-rules.xml定
义了规则的Digester的实例。下面的代码创建
了一个Digester,通过将person-rules.xml的URL传递给DigesterLoader
既然person-rules.xml文件是与解析它的类在同一个包内的类路径资源,URL可以通过getClass().getResource()来得到。DigesterLoader然后解析规则并将它加到新创建的Digester上:
import org.apache.commons.digester.Digester;
import org.apache.commons.digester.xmlrules.DigesterLoader;
// 从XML规则集中配置Digester
URL rules = getClass().getResource("./person-rules.xml");
Digester digester =
DigesterLoader.createDigester(rules);
// 将空的List推入到Digester的堆栈
List people = new ArrayList();
digester.push( people );
// 解析XML文档
InputStream input = new FileInputStream( "data.xml" );
digester.parse( input );
一旦Digester完成对data.xml的解析,三个Person对象将会在ArrayList people中。
与
将规则定义在XML不同的方法是使用简便的方法将它们加入到一个Digester实例中。大多数文章和例子都用这种方法,使用
addObjectCreate() 和
addBeanPropertySetter()这样的方法来将规则加入中Digester上。下面的代码加入了与定义在person-
rules.xml中相同的规则:
digester.addObjectCreate("people/person", Person.class);
digester.addSetNext("people/person", "add", "java.lang.Object");
digester.addBeanPropertySetter("people/person", "name");
digester.addBeanPropertySetter("people/person", "age");
如果你曾经发现自己正在用一个有着2500行代码的类,用SAX来解析一个巨大的XML文档,或者
使用DOM或JDOM的完整的一个集合类,你就会理解XML的解析比它应该做的要复杂的多,就大多数情况来说。如果你正在建一个有着严格的速度和内存要求
的高效的系统,你会需要SAX解析器的速度。如果你需要DOM级别3的复杂度,你会需要像Apache
Xerces的解析器。但如果你只是简单的试图将几个XML文档解析到对象中去的话,看一下Commons Digester,
并把你的规则定义在一个XML文件中。
任何时候你都应该将配置信息从硬编码中移出来。我会建议你在一个XML文件中定义规则并从文件系统
或类路径中装入它。这样可以使你的程序更好地适应XML文档以及对象模型的变化。有关在XML文件中定义Digester规则的更多的资料,参看
Jakarta Commons Cookbook一书的6.2节,“将XML文档转换为对象”
2.Commons Collections中的算子
算
子成为Commons Collections
3.1中的有趣的部分有两个原因:它们没有得到应得的重视并且它们有改变你编程的方式的潜力。算子只是一个奇特的名字,它代表了一个包装了函数的对象—一
个“函数对象”。当然,它们不是一回事。如果你曾经使用过C和C++的方法指针,你就会理解算子的威力。
一个算子是一个对象—一个Predicate,一个Closure, 一个Transformer。
Predicates
求对象的值并返回一个boolean,Transformer求对象的值并返回新对象,Closure接受对象并执行代码。算子可以被组合成组合算子来模
仿循环,逻辑表达式,和控制结构,并且算子也可以被用来过滤和操作集合中的元素。在这么短的篇幅中解释清楚算子是不可能的,所以跳过介绍,我将会通过使用
和不使用算子来解决同一问题(解释算子)。在这个例子中,从一个ArrayList中而来的Student对象会被排序到两个List中,如果他们符合某
种标准的话。
成绩为A的学生会被加到honorRollStudents(光荣榜)中,得D和F的学生被加到
problemStudents
(问题学生)list中。学生分开以后,系统将会遍历每个list,给加入到光荣榜中学生一个奖励,并安排与问题学生的家长谈话的时间表。下面的代码不使
用算子实现了这个过程:
List allStudents = getAllStudents();
// 创建两个ArrayList来存放荣誉学生和问题学生
List honorRollStudents = new ArrayList();
List problemStudents = new ArrayList();
// 遍历所有学生,将荣誉学生放入一个List,问题学生放入另一个
Iterator allStudentsIter = allStudents.iterator();
while( allStudentsIter.hasNext() ) {
Student s = (Student) allStudentsIter.next();
if( s.getGrade().equals( "A" ) ) {
honorRollStudents.add( s );
} else if( s.getGrade().equals( "B" ) &&
s.getAttendance() == PERFECT) {
honorRollStudents.add( s );
} else if( s.getGrade().equals( "D" ) ||
s.getGrade().equals( "F" ) ) {
problemStudents.add( s );
} else if( s.getStatus() == SUSPENDED ) {
problemStudents.add( s );
}
}
// 对于的有荣誉学生,增加一个奖励并存储到数据库中
Iterator honorRollIter =
honorRollStudents.iterator();
while( honorRollIter.hasNext() ) {
Student s = (Student) honorRollIter.next();
// 给学生记录增加一个奖励
s.addAward( "honor roll", 2005 );
Database.saveStudent( s );
}
// 对所有问题学生,增加一个注释并存储到数据库中
Iterator problemIter = problemStudents.iterator();
while( problemIter.hasNext() ) {
Student s = (Student) problemIter.next();
// 将学生标记为需特殊注意
s.addNote( "talk to student", 2005 );
s.addNote( "meeting with parents", 2005 );
Database.saveStudent( s );
}
上述例子是非常过程化的;要想知道Student对象发生了什么事必须遍历每一行代码。例子的第一部分是基于成绩和考勤对Student对象进行逻辑判断。
第二部分对Student对象进行操作并存储到数据库中。像上述这个有着50行代码程序也是大多程序所开始的—可管理的过程化的复杂性。但是当需求变化时,问题出现了。一旦判断逻辑改变,你就需要在第一部分中增加更多的逻辑表达式。
举
例来说,如果一个有着成绩B和良好出勤记录,但有五次以上的留堂记录的学生被判定为问题学生,那么你的逻辑表达式将会如何处理?或者对于第二部分中,只有
在上一年度不是问题学生的学生才能进入光荣榜的话,如何处理?当例外和需求开始改变进而影响到过程代码时,可管理的复杂性就会变成不可维护的面条式的代
码。
从上面的例子中回来,考虑一下那段代码到底在做什么。它在一个List遍历每一个对象,检查标准,如果适用该标准,对此对象进行某些
操作。上述例子可以进行改进的关键一处在于从代码中将标准与动作解藕开来。下面的两处代码引用以一种非常不同的方法解决了上述的问题。首先,荣誉榜和问题
学生的标准被两个Predicate对象模型化了,并且加之于荣誉学生和问题学生上的动作也被两个Closure对象模型化了。这四个对象如下定义:
import org.apache.commons.collections.Closure;
import org.apache.commons.collections.Predicate;
// 匿名的Predicate决定一个学生是否加入荣誉榜
Predicate isHonorRoll = new Predicate() {
public boolean evaluate(Object object) {
Student s = (Student) object;
return( ( s.getGrade().equals( "A" ) ) ||
( s.getGrade().equals( "B" ) &&
s.getAttendance() == PERFECT ) );
}
};
//匿名的Predicate决定一个学生是否是问题学生
Predicate isProblem = new Predicate() {
public boolean evaluate(Object object) {
Student s = (Student) object;
return ( ( s.getGrade().equals( "D" ) ||
s.getGrade().equals( "F" ) ) ||
s.getStatus() == SUSPENDED );
}
};
//匿名的Closure将一个学生加入荣誉榜
Closure addToHonorRoll = new Closure() {
public void execute(Object object) {
Student s = (Student) object;
// 对学生增加一个荣誉记录
s.addAward( "honor roll", 2005 );
Database.saveStudent( s );
}
};
// 匿名的Closure将学生标记为需特殊注意
Closure flagForAttention = new Closure() {
public void execute(Object object) {
Student s = (Student) object;
// 标记学生为需特殊注意
s.addNote( "talk to student", 2005 );
s.addNote( "meeting with parents", 2005 );
Database.saveStudent( s );
}
};
这
四个匿名的Predicate和Closure是从作为一个整体互相分离的。flagForAttention(标记为注意)并不知道什么是确定一个问题
学生的标准 。现在需要的是将正确的Predicate和正确的Closure结合起来的方法,这将在下面的例子中展示:
import org.apache.commons.collections.ClosureUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.functors.NOPClosure;
Map predicateMap = new HashMap();
predicateMap.put( isHonorRoll, addToHonorRoll );
predicateMap.put( isProblem, flagForAttention );
predicateMap.put( null, ClosureUtils.nopClosure() );
Closure processStudents =
ClosureUtils.switchClosure( predicateMap );
CollectionUtils.forAllDo( allStudents, processStudents );
在
上面的代码中,predicateMap将Predicate与Closure进行了配对;如果一个学生满足作为键值的Predicate的条件,那么它
将把它的值传到作为Map的值的Closure中。通过提供一个NOPClosure值和null键对,我们将把不符合任何Predicate条件的
Student对象传给由ClosureUtils调用创建的“不做任何事”或者“无操作”的NOPClosure。
一个
SwitchClosure,
processStudents,从predicateMap中创建。并且通过使用CollectionUtils.forAllDo()方法,将
processStudents
Closure应用到allStudents中的每一个Student对象上。这是非常不一样的处理方法;记住,你并没有遍历任何队列。而是通过设置规则
和因果关系,以及CollectionUtils和SwitchClosur来完成了这些操作。
当你将使用Predicate的标准与使
用Closure的动作将分离开来时,你的代码的过程式处理就少了,而且更容易测试了。isHonorRoll
Predicate能够与addToHonorRoll
Closure分离开来进行独立的单元测试,它们也可以合起来通过使用Student类的模仿对象进行测试。第二个例子也会演示
CollectionUtils.forAllDo(),它将一个Closure应用到了一个Collection的每一个元素中。
你也许注意到了使用算子并没用减少代码行数,实际上,使用算子还增加了代码量。但是,通过算子,你得到了将到了标准与动作的模块性与封装性的好处。如果你的代码题已经接近于几百行,那么请考虑一下更少过程化处理,更多面向对象的解决方案—通过使用算子。
Jakarta Commons Cookbook中的第四章“算子”介绍了Commons Collections中可用的算子,在第五章,“集合”中,向你展示了如何使用算子来操作Java 集合类API。
所
有的算子-- Closure, Predicate, 和 Transformer—能够被合并为合并算子来处理任何种类的逻辑问题。switch,
while和for结构能够被SwitchClosure, WhileClosure, 和 ForClosure模型化。
复合的逻辑表达式可
以被多个Predicate构建,通过使用OrPredicate, AndPredicate, AllPredicate, 和
NonePredicate将它们相互联接。Commons BeanUtils也包含了算子的实现被用来将算子应用到bean的属性中--
BeanPredicate, BeanComparator, 和
BeanPropertyValueChangeClosure。算子是考虑底层的应用架构的不一样的方法,它们可以很好地改造你编码实现的方法。
3. 使用XPath语法来查询对象和集合
Commons JXPath是一种让人很吃惊地(非标准的)对XML标准的使用。XPath一段时间以来一直是作为在一个XSL样式表中选择结点或结点集的一种方法。如果你用过XML,你会很熟悉用这样的语法/foo/bar来从foo文档元素中选择bar子元素。
Jakarta
Commons
JXPath增加了一种有趣的手法:你可以用JXPath来从bean和集合中选择对象,其中如servlet上下文和DOM文档对象。考虑一个包含了
Person对象的列表。每一个Person对象有一个属性的类型为Job,每一个Job对象有一个salary(薪水)属性,类型为int。
Person对象也有一个coountry属性,它是两个字符的国家代码。使用JXPath,你可以很容易地选出所有国家为美国,薪水超过一百万美元的
Person对象。下面是设置一个由JXPath过滤地bean的List的代码:
// Person的构造器设置姓和国家代码
Person person1 = new Person( "Tim", "US" );
Person person2 = new Person( "John", "US" );
Person person3 = new Person( "Al", "US" );
Person person4 = new Person( "Tony", "GB" );
// Job的构造器设工作名称和薪水
person1.setJob( new Job( "Developer", 40000 ) );
person2.setJob( new Job( "Senator", 150000 ) );
person3.setJob( new Job( "Comedian", 3400302 ) );
person4.setJob( new Job( "Minister", 2000000 ) );
Person[] personArr =
new Person[] { person1, person2,
person3, person4 };
List people = Arrays.asList( personArr );
people
List包含了四个bean: Tim, John, Al,
和George。Tim是一个挣4万美元的开发者,John是一个挣15万美元的参议员,Al是一个挣340万美元的喜剧演员,Tony是一个挣200万
欧元的部长。我们的任务很简单:遍历这个List,打印出每一个挣钱超过100百万美元的美国公民的名字。记住people是一个由Person对象构成
的ArrayList,让我们先看一下没有利用JXPath便利的解决方案:
Iterator peopleIter = people.getIterator();
while( peopleIter.hasNext() ) {
Person person = (Person) peopleIter.next();
if( person.getCountry() != null &&
person.getCountry().equals( "US" ) &&
person.getJob() != null &&
person.getJob().getSalary() > 1000000 ) {
print( person.getFirstName() + " "
person.getLastName() );
}
}
}
}
上
面的例子是繁重的,并有些容易犯错。为了发现合适的Person对象,你必须首先遍历每一个Person对象并且检查conuntry的属性。如果
country属性不为空并且符合要求,那么你就要检查job属性并看一下它是否不为空并且salary属性的值大于100万。上面的例子的代码行数可以
被Java 1.5的语法大大减少,但是,哪怕是Java 1.5,你仍旧需要在两层上作两次比较。
如果你想对内存中的一组Person对象也做一些这样的查询呢?如果你的应用想显示所有在英格兰的名叫Tony的人呢?喔,如果你打印出每一个薪水少于2万的工作的名称呢?
如
果你将这些对象存储到关系数据库中,你可以用一个SQL查询来解决问题,但你正在处理的是内存中的对象,你可以不必那么奢侈。虽然XPath主要是用在
XML上面,但你可以用它来写一个针对对象集合的“查询”,将对象作为元素和,把bean属性作为子元素。是的,这是一种对XPath奇怪的应用,但请先
看一下下面的例子如何在people上,一个由Person对象构成的ArrayList,实现这三种查询:
import org.apache.commons.jxpath.JXPathContext;
public List queryCollection(String xpath,
Collection col) {
List results = new ArrayList();
JXPathContext context =
JXPathContext.newContext( col );
Iterator matching =
context.iterate( xpath );
while( matching.hasNext() ) {
results.add( matching.getNext() );
}
return results;
}
String query1 =
".[@country = 'US']/job[@salary > 1000000]/..";
String query2 =
".[@country = 'GB' and @name = 'Tony']";
String query3 =
"./job/name";
List richUsPeople =
queryCollection( query1, people );
List britishTony =
queryCollection( query2, people );
List jobNames =
queryCollection( query3, people );
queryCollection()方法使用了一个XPath表达式,将它应用到一个集合上。
XPath表达式被JXPathContext求值,
JXPathContext由JXPathContext.newContext()调用创建,并将它传入要执行查询的集合中。凋用
context.iterate()来在集合中的每一个元素上应用XPath表达式,返回包含所有符合条件的“节点”(这里是“对象”)的
Iterator。上例中执行的第一个查询,query1,执行了和不使用JXPath的例子相同的查询。query2选择所有国家为GB并且名字属性为
Tony的Person对象,query3返回了一个String对象的List,包含了所有Job对象的name属性。
当我第一次看到
Commons JXPath,
它是一个坏思想的想法触动了我。为什么要把XPath表达式应用到对象上?有点感觉不对。把XPath作为一个bean的集合的查询语言的这种意想不到的
用法,在过去几年中已经好多次给我带来了便利。如果你发现你在list中循环来查找符合条件的元素,请考虑一下JXPath。更多的信息,请参考
Jakarta Commons Cookbook的第12章,“查找和过滤”,它讨论了Commons JXPath和与Commons
Digester配对的Jakarta Lucene。
还有更多
对Jakarta
Commons纵深地探索仍然在调试中。在这一系列的下面几部分中,我会介绍一些相关的工具和功能。在Commons
Collections中设置操作,在collection中使用Predicate对象,使用Commons
Configuration来配置一个应用和使用Commons Betwixt来读写XML。能从Jakarta
Commons得到的东西还有很多,不能在几千字中表达,所以我建议你看一下Jakarta Commons
Cookbook。许多功能可能会,一眼看上去,有点普通,但Jakarta Commons的能量就蕴藏在这些工具的相互组合和与你的系统的集成当中。
Timothy M. O'Brien是一个专业的独立的开发者,在Chicago地区工作和生活。
资源
·onjava.com:onjava.com
·Matrix-Java开发者社区:http://www.matrix.org.cn/
·APACHE:APACHE.org