http://www.dingl.com/轉來的

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,創建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框架之中。

  結束語:第三篇(也是最后一篇)介紹 Jakarta Commons的文章就到這里結束。雖然這個系列的文章只涉及了各個組件的基礎知識,但希望它們已經足以讓你開始下一步的深入研究。