如果你不熟悉Jakarta
Commons話,那么很有可能你已經重新發明了好幾個輪子。在你編寫更多的普通的框架或工具之前,體驗一下Commons吧。它將會大大地節約你的時
間。太多的人自己寫一個,其實是與Commons
Lang中的StringUtils重復的StringUtils類,或者,開發者不知道從Commons
Collections中重新創建工具,哪怕commons-collections.jar已經在classpath中可用了。
真的,
請停一下??纯碈ommons 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規則?,F在你需要創建一個由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