在本系列的上一部分中,我演示了如何取出 XML 文檔並將它轉換成 Java 表示。這種變換的關鍵是 XML 文檔符合的 XML 模式。模式不僅確保了強制約束。它還允許使用 SchemaMapper 來生成 Java 類;那麼 XML 文檔就可以解包成那些類其中一個的實例。換句話說,這個系統不僅需要 XML 文檔;文檔將變成其實例的 Java 類不僅必須已經存在,而且它還必須在系統的類路徑中。
打包類
打包 Java 實例時,情況稍有不同。首先,Unmarshaller 類不存儲關於在所創建的 Java 實例中使用的 XML 模式的信息。因此,從 XML 文檔創建的 Java 類的實例與任何其它 Java 類沒有本質區別。最初,這似乎是一個問題。Java 類將實例轉回 XML 是否必須采取更多的操作?答案是“是”。然而,這卻是好事:從 XML 解包的類完全獨立於該 XML 文檔。這有以下幾個優點:
Java 實例可以解包成不同的 XML 文檔。
打包代碼不依賴於查找 XML 模式或 XML 文檔。
對這個 Java 實例使用反射和其它 Java 方法不會產生意外的字段或方法。
前兩點可能是顯而易見的,而第三點也許不是。Java 應用程序中正越來越廣泛地使用反射,以在運行時處理各種不同類型的對象。通常會檢查字段和方法以確定如何使用對象。如果要創建附加方法(如 toXML())或附加字段(如 xmlSchema),類會將這些方法返回到檢查它的應用程序。最終結果是其它應用程序代碼可能會錯誤地使用特別用於打包和解包數據的信息。在數據綁定的環境之外,很可能會誤用這些方法和字段(或變量)。它們甚至會給出不該披露的網絡資源(如 XML 模式)的應用程序信息。
正是由於這些原因,有必要承認將 Java 打包成 XML 比使用原來的 XML 模式進行打包略有難度。但是,因為有了這個難度,生成的 Java 實例才獨立於 XML 文檔或模式。如果所有這些令您感到困惑,請按以下方式考慮這個問題:可以將已解包的 Java 對象和已打包的 XML 文檔返回給其它應用開發者,且不必給出任何特定的用法指令。實際上,他們甚至不知道數據原來是 XML 還是 Java;他們所看到的數據格式只是您提供的格式。
這是一條要記住的重要原則 -- 隔離數據檢索方法。那就更容易遵守使數據盡可能保持專用的基本指南。例如,關於如何從 XML 文檔中獲取數據的信息並不適合分發給其它應用開發者,特別是他們在不同的公司(如在商家對商家應用程序中)。
深入研究
我已經講述了一些預備概念,現在該研究代碼了。首先,需要添加一個入口點,以便將 Java 對象打包成 XML 文檔。如同 Unmarshaller 類,其它程序最容易從靜態方法中使用打包代碼。如果沒有選項需要傳入 Java 對象,就象這裡討論的情況,這種方式很有效。同時,遵守好的編程慣例、僅內部使用這個靜態方法構造對象,以及調用非靜態方法也很重要。這個靜態方法是打包 Java 對象的“前門”。除了取出對象進行打包,靜態方法還應該可以輸出 XML 文檔。
這裡,需要避免一個常見錯誤,即允許將 XML 文檔輸出成 String 或 File。這兩種輸出方法假設將對象打包成某些永久存儲,並保存在物理硬盤上。盡管,對象經常通過網絡流傳遞,如傳遞到另一個應用程序。或者它可以交給 XSLT 處理器,該處理器將 XML 變換成其它形式。這兩種情況下,String 或 File 都不能完全解決問題。實際上,應該選取一個較安全的路由,並允許提供用於寫入的 OutputStream。這個流可以封裝 FileOutputStream,以便寫入本地文件、網絡連接或者到另一個程序的管道(如剛提到的 XSLT 處理器)。確定了這個自變量後,才可以考慮 Marshaller 類的入口點。
清單 1. 靜態入口點 /**
* <p>
* This method is the public entry point for marshalling an object into
* an XML instance document.
* </p>
*
* @param obj <code>Object</code> to convert to XML.
* @param out <code>OutputStream</code> to write XML to.
* @throws <code>IOException</code> when errors in output occur.
*/
public static void marshall(Object obj, OutputStream out) throws IOException {
Marshaller marshaller = new Marshaller();
marshaller.writeXMLRepresentation(obj, out);
}
看上去很簡單,是嗎?很好。除了文章中的這個和其它代碼片段,還可以 下載完整的 Marshaller 類 ,並 查看 HTML 格式的類 。
一旦內部創建了 Marshaller類的實例,提供給靜態方法的變量就傳遞到同一方法的動態版本。因為我已經讓所提供的流寫入結果,所以不需要返回值。建造了前門之後,就該研究房子的其余部分了。
聲明 XML 模式無關性
就象我以前提到過的,不能依賴 XML 模式或任何插到 Java 類中的方法來簡化從 Java 到 XML 的轉換。相反,我們必須從 Java 類中的數據手工創建 XML 文檔。但是,這種決定非常重大的副作用是您可能采用 任何 符合 JavaBean 式樣格式的 Java 類來生成 XML 表示。我說 JavaBean 式樣 是因為不必實現 JavaBean 接口,但對於類,希望它對接口的所有數據具有讀方法。例如,如果類有一個名為 name 的字段,則希望有一個返回該值的方法 getName。任何沒有象這樣的讀方法的數據字段都將導致在打包過程中忽略該數據。
強制了這種簡單的約束(並且這是標准編程慣例)之後,就可以將任何 Java 對象轉換成 XML。這還允許來回轉換 XML 數據。XML 文檔可以轉換成 Java 實例,然後再轉換回 XML。但是,有一個告誡:原始 XML 模式聲明會丟失!請記住,這就是我們的 Java 打包過程不依賴 XML 模式的副作用。打包得到的 XML 輸出完全不知道任何約束 XML 的 XML 模式。再次將 XML 解包成 Java 時,這不會引起問題,因為類路徑中只需要包含必須創建其新實例的表示對象的類。您可以回過去看一下第三部分,我在解包過程中沒有使用 XML 模式。因此,將 XML 轉換成 Java,再轉換回 XML 並不會發生問題,只是會丟失模式約束。當然,本系列中的所有代碼都是開放源碼,因此如果您的應用程序中需要這個功能,歡迎您將此功能添加到應用程序中。
然後,主方法比較簡單。它接受一個對象,應該返回該對象的 XML 元素表示。如果對象包含對其它 Java 對象的引用,那麼可以使用相同的方法進行遞歸,並將一個“子”元素添加到頂級元素。這個最終元素被返回到調用方法,並輸出到所提供的流。(我將在下一節中討論此最終方法。)因為 XML 的 JDOM 表示易於操作代碼,所以仍使用它。清單 2 中顯示了這個核心方法。
清單 2. 將 Java 對象轉換成 XML 的核心方法 /**
* <p>
* This is the granular portion of binding; a Java <code>Object</code> is
* converted into an XML element (in JDOM form), using recursion for any
* children.
* </p>
*
* @param obj <code>Object</code> to get the XML element representation for.
* @return <code>Element</code> - representation of <code>Object</code> in XML.
* @throws <code>IOException</code> when errors occur in binding.
*/
private Element getXMLRepresentation(Object obj) throws IOException {
Class objectClass = obj.getClass();
// Get the name of the element for this object
String objectName = BindingUtils.initialLowercase(objectClass.getName());
// If this is an "Impl" class, remove that from the name
int index = -1;
if ((index = objectName.indexOf("Impl")) != -1) {
objectName = objectName.substring(0, index);
}
Element element = new Element(objectName);
Method[] methods = objectClass.getMethods();
for (int i=0; i<methods.length; i++) {
// Only want accessor methods, but not the getClass() method in Object
Method method = methods[i];
if ((method.getName().startsWith("get")) &&
(!method.getName().equals("getClass"))) {
// Get the value for this method
try {
Object o = method.invoke(obj, new Object[] { });
// For the name, remove the "get" and lower the initial letter
String propertyName =
BindingUtils.initialLowercase(method.getName().substring(3));
// Determine if it's primitive by seeing if it's a java.lang type
if (o.getClass().getName().startsWith("java.lang.")) {
// If it's a primitive, add as an attribute
element.addAttribute(propertyName, o.toString());
} else { // ... otherwise, recurse and add new element as child
element.addContent(getXMLRepresentation(o));
}
} catch (IllegalAccessException e) {
throw new IOException(e.getMessage()); } catch (InvocationTargetException e) {
throw new IOException(e.getMessage()); }
}
}
return element;
}
可以看到,代碼非常簡單。首先,獲取 Java 類的名稱。這個名稱將變成正在構造的 XML 表示的元素名稱。BindingUtils 中一個新的實用程序方法用於將此名稱轉換成以小寫字母開頭的名稱。(標准 XML 名稱都以小寫字母開頭。)在 參考資料 節中可以下載更新的 BindingUtils 類,以及代碼包的其余部分。另外,如果類是 Impl 類,將除去名稱的 "Impl" 部分。請記住,在本系列的第三部分中,類就是接口(如 WebServiceConfiguration)和實現(如 WebServiceConfigurationImpl)。此外,這是標准編程慣例,並且該方法將在普通情形中起作用。
一旦元素名稱就緒,則必須獲取屬性。每個屬性都應該是字段名稱和字段的值。將任何復雜對象(非 Java 原語)轉換成嵌套元素。要獲取此數據,可以使用反射來獲取對象的所有可用方法。您只需要關心讀方法(以 "get" 開頭);其余可以忽略,包括從 java.lang.Object 繼承的 getClass() 方法。然後,(再次)使用反射來調用方法,獲取它的值。最後,可以再使用 BindingUtils 類從其方法派生出字段名稱。因此,方法 getVersion() 將導致創建一個叫做 "version" 的屬性,它的值通過調用 getVersion() 方法產生。
您將注意到以上最後要指出的是字段是 Java 原語(其中類在 java.lang包中)還是嵌套對象。前一種情況下,屬性添加到整個元素。後一種情況下,將發生遞歸並創建一個子元素。一旦以這種方式處理了每個讀方法,生成的 JDOM 就返回到調用程序。一旦展開了遞歸,其結果就是頂級 Java 對象的完整的 XML 表示,它不能用作 XML 文檔的根元素。
完成接觸
只需一點努力,您就學會了打包代碼。所剩下的就是在輸入方法和遞歸方法之間的縫隙之上建立橋梁。使用 JDOM 輸出類 org.jdom.output.XMLOutputter 可以很容易就跨越這個縫隙。這個類使用 JDOM Document 和 OutputStream,輸出 XML。當然,我們有一個流可以使用,還可以將從剛討論過的方法中返回的元素作為文檔根元素使用,來創建一個簡單的文檔。將這個元素和流傳遞到 XMLOutputter 中並調用 output() 方法來實現這個技巧。清單 3 中顯示了這個方法;它由靜態 marshall 方法調用,並使用我們剛討論過的方法。
清單 3. 將各個細節與 output() 方法連接 /**
* <p>
* This will take a Java object instance, and convert it into an
* XML document, and write that document to the supplied output stream.
* </p>
*
* @param obj <code>Object</code> to convert to XML.
* @param out <code>OutputStream</code> to write XML to.
* @throws <code>IOException</code> when errors in output occur.
*/
private void writeXMLRepresentation(Object obj, OutputStream out)
throws IOException {
// Root Element is the start of recursion
Element root = getXMLRepresentation(obj);
Document doc = new Document(root);
// Use 2 space indentation and line feeds
XMLOutputter outputter = new XMLOutputter(" ", true);
outputter.output(doc, out);
}
就象任何好的代碼一樣,您應該放下理論,實際使用此代碼。本文的其余部分討論了在實際情況下,首先使用 Marshaller 類,然後使用整個 Marshaller 包。所以,讓我們將這些類投入實際使用。
實踐出真知
就象任何好的代碼一樣,您應該放下理論,實際使用此代碼。本文的其余部分討論了在實際情況下,首先使用 Marshaller 類,然後使用整個 org.enhydra.xml.binding 包。所以,讓我們將這些類投入實際使用。
請記住,在本系列的第三部分中,測試 Unmarshaller 類時做的第一件事就是編寫一個相當簡單的類 TestMapper。盡管這個類只能對解包進行基本測試,但它卻是開發數據綁定類過程中的關鍵部分。當然,在任何應用程序中,編碼新功能後的第一件事就是針對該功能編寫一個非常基本的測試。在將新功能放到一個大應用程序中的過程中(通常是件好玩的事),這通常只是處理隱蔽錯誤的好方法。而有一個測試類可以適用於每個應用程序類,有時適用於每個類的方法(是的,您沒有看錯),可以節省您的調試時間。有幾種好的結構可以幫助自動執行這些類型的測試:JUnit 是一個很棒的免費測試包、JTest 是一個很好的需付費測試包。請您的公司投資購買一個測試包吧,長期使用後您會發現它物超所值。
在我鼓吹了實際應用的重要性之後,我將討論這個測試類。加上它,可以測試 Unmarshaller 和 Marshaller 類。是的,我知道這破壞了我剛談到的規則,但為了使本文的篇幅控制在 20 頁以內,我只能這麼做。清單 4 中顯示了這個類,其中的更新可以幫助測試新的類。
清單 4. 測試 Marshaller import java.io.File;
import org.enhydra.xml.binding.Marshaller;
import org.enhydra.xml.binding.Unmarshaller;
public class TestMapper {
public static void main(String[] args) {
System.out.println("Starting unmarshalling...");
try {
System.out.println("\n\n......... Start of Unmarshaller test ............\n\n");
File file = new File("xml/example.xml");
Object o = Unmarshaller.unmarshall(file.toURL());
System.out.println("Object class: " + o.getClass().getName());
System.out.println("Casting to WebServiceConfiguration...");
WebServiceConfiguration config = (WebServiceConfiguration)o;
System.out.println("Successful cast.");
System.out.println("Name: " + config.getName());
System.out.println("Version: " + config.getVersion());
System.out.println("Port Number: " + config.getPort().getNumber());
System.out.println("Port Protocol: " + config.getPort().getProtocol());
System.out.println("\n\n......... End of Unmarshaller test ............\n");
System.out.println("\n\n......... Start of Marshaller test ............\n\n");
Marshaller.marshall(o, System.out);
System.out.println("\n\n......... End of Unmarshaller test ............\n");
} catch (Exception e) { e.printStackTrace();
}
}
}
新的代碼以突出顯示的字體顯示(還有自上一篇文章後添加到測試類的一些有用的調試消息)。新代碼的第一段讀取第三部分中涵蓋的 XML 文檔,創建該文檔的 Java 表示,並打印出關於已解包數據的信息。然後,將 Java 對象打包回 XML,並將其結果放到系統的 OutputStream,當然輸出到屏幕上。運行 TestMapper 程序時,其輸出類似於清單 5。
清單 5. 最終結果 $ (/projects/dev/mapper:bmclaugh)> java TestMapper xml/configuration.xsd
Starting unmarshalling...
......... Start of Unmarshaller test ............
Object class: WebServiceConfigurationImpl
Casting to WebServiceConfiguration...
Successful cast.
Name: Unsecured Web Listener
Version: 1.1
Port Number: 80
Port Protocol: http
......... End of Unmarshaller test ............
......... Start of Marshaller test ............
<?xml version="1.0" encoding="UTF-8"?>
<webServiceConfiguration name="Unsecured Web Listener" version="1.1">
<portType protocol="http" number="80" protectedPort="false" />
<documentType index="*.html,*.xml" root="/usr/local/enhydra/html" error="error.html" />
</webServiceConfiguration>
......... End of Unmarshaller test ............
乍看,這裡顯示的 XML 輸出與您本地機器上的 XML 文檔(可以從 參考資料 部分的一個鏈接中下載)差別很大。但是,仔細觀察之後,可以發現兩個文檔之間只有很少差異。如果忽略元素之間的間隔和縮排,所有屬性及其值都與輸入文檔完全相同。唯一的區別就是 XML 模式引用(和關聯名稱空間)不見了,正如缺省名稱空間聲明一樣。正如我早先討論過的,這是有意的,使 Java 對象可以獨立於其余數據綁定代碼而存在。至於缺省名稱空間,必須在 Unmarshaller 創建 Java 對象時將關於該名稱空間的某些信息存儲到該對象中,以便保存。在 XML 應用程序中,可以選擇關閉名稱空間處理(使帶缺省名稱空間的元素等價於不帶任何名稱空間的元素,因為兩者都沒有前綴),或者可以修改代碼以使它適合您的特殊需要。
在這兩種情況下,都可以清楚地看到,有一個從 Java 對象創建 XML 文檔的功能性過程。甚至可以插入其它 Java 對象 -- 包括不是從 XML 創建的 Java 對象 -- 還可以查看它們的 XML 表示。我們接著在更深的層次上討論 Marshaller 類,並在更實際的示例中使用它。
討論 Web 服務
還記得我要討論的 Web 服務嗎?它又回來了。前一部分討論了啟動 Web 服務是多麼簡單,以及如何使用 XML 配置文檔來存儲其數據。將該數據解包成 Java 對象允許 Web 服務將 XML 數據作為 Java 變量放到它的方法中。數據綁定使獲取 XML 數據的過程變得簡單且直接,不必處理 DOM 或者研究 SAX。
現在,讓我們看看另一方面:關閉 Web 服務並存儲數據。這很平常,服務在一個端口上啟動,(例如)還有一個文檔根和錯誤頁面,然後它最終有許多數據字段的值改變了。用戶管理服務,同時會做一些修改。但是,關閉服務器時不存儲數據將使這些更改丟失。要將此數據放回到原來的 XML 文檔中很簡單,請使用清單 6 中顯示的 Marshaller。
清單 6. 啟動和停止 Web 服務 import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import myApp.WebServiceConfiguration;
public class WebService() {
/** File to write configuration data to */
private File configurationFile;
/** Configuration variables */
private WebServiceConfiguration config;
public WebService(String configurationFile) throws IOException {
this(new File(configurationFile));
}
public WebService(File configurationFile) throws IOException) {
this.configurationFile = configurationFile;
}
/**
* Various mutator methods for configuration data would be included.
* Each would "proxy" through and set the config object's data.
*/
public void start() throws IOException {
// Obtain the configuration
config = Unmarshaller.unmarshal(configurationFile.toURL());
}
public void stop() throws IOException {
// Save the configuration object
Marshaller.marshal(config, new FileOutputStream(configurationFile));
}
}
可以看到,我將 Web 服務的初始配置移到 start() 方法。服務的構造器接受從中裝入配置數據的文件。然後,構造器將數據保存到同一個文件中。在 stop() 方法中永久保存數據。另外,服務的所有數據都存儲在 config(它存儲服務使用的基本數據)中,而不必使用多個成員變量(如 portNumber 或 name)。這就是以後要永久保存的對象。除了使編寫 start() 和 stop() 方法變得很簡單(每個方法只有一行!),這個方法還允許 Web 服務存儲其它本來就是“臨時的”且無需永久保存的數據。
當然,類也許還包括了這裡沒有談到的其它方法。但是,已經討論的一些方法顯示了裝入和存儲 XML 數據是多麼簡單,甚至不必知道 XML。那麼,看了這個例子之後,還什麼要討論呢?只有一小部分代碼更新,然後就完成了。
逐步發展的 API(續)
如果您認為所看到的內容是重復的,或者這個標題是從本系列第三部分中抄過來的,不用擔心。正如 JDOM 從第二部分到上一篇文章的不斷變化一樣,從上一篇文章到本書中,它也不斷變化著。實際上,最近發行了 JDOM Beta 5 -- 它與前一版本的區別很大。要實現這些改進的功能,數據綁定代碼也要不斷改變。幸好,從上一篇文章到本文中,代碼中的許多更改都不是很重要,而您的老版本仍可以照常運行。但我還是建議您使用 參考資料 中的鏈接,獲取各種數據綁定類的最新版本。我在本文的最後編輯階段,已經用 JDOM 的最新版本(Beta 5)對它們進行了測試。所以,如果您手邊是老版本的 JDOM,或者此代碼的老版本,或者都是老版本,請利用這個機會升級到最新同時也是最棒的版本。
結束語
您已經通讀了這四篇全面深入的數據綁定文章。如果您已經開始使用尖括號,並且不常使用空格,不必擔心;這可以高級玩意!太好了,您已經開始看到這種方法的強大功能了,並且已經考慮如何在應用程序中使用數據綁定類了。下次您編寫配置文件、分析 XML 文檔語法或者從 Java 轉換成永久存儲時,請考慮數據綁定是否可以使您的作業更簡單或更有效。
最後請注意:隨著本系列即將結束,您不必考慮代碼的發展。文章中提到的代碼將並入 Enhydra 應用程序服務器結構中。當您閱讀本文時,Enhydra 站點上應該有存放此代碼的公司 FTP 服務器的鏈接可供下載使用。它將被不斷維護、增強,並且不斷反復,直到它可以合並到實際的 Enhydra 應用程序服務器中。所以,請與我們一起使這個開放源碼項目變成更實用、更有效、更適合每個人使用的好工具。我們以後還會見面,也許在 Enhydra,也可能是我即將推出的關於 SOAP、JSP 以及許多更有趣話題的文章中。