Brett McLaughlin, 作家/編輯, O‘Reilly Media, Inc.
2004 年 9 月 01 日 在 上一篇文章中,Brett 幫助您對 JaxMe API 有了深入的了解。在這一基礎上,本文將說(shuō)明如何將 XML 文檔轉化成 Java 類(lèi)實(shí)例、操縱底層的 XML 數據然后再把修改后的數據轉換成 XML。本文將為您提供翔實(shí)的 JaxMe 應用知識,以便在您的應用程序編寫(xiě)中加以運用。 首先要指出我希望您已經(jīng)讀過(guò)本系列文章的 上一篇,事實(shí)上我將沿用那篇文章中的例子,如果您沒(méi)有按照順序閱讀可能會(huì )有點(diǎn)手足無(wú)措。 迭代的過(guò)程 本文將指出數據綁定的迭代特性,這也是您需要真正注意的一點(diǎn)。很多 API,特別是那些屬于 工具類(lèi) 的 API,都只需要放入類(lèi)路徑然后直接使用即可,Jakarta Commons 類(lèi)就是一個(gè)很好的例子。就是說(shuō)只要放到 Java 工具組中就隨時(shí)都可以使用。但事實(shí)上,數據綁定 API 的工作方式有點(diǎn)不同,人們很少會(huì )到處使用數據綁定中的方法,而是在應用程序的某一部分集中使用數據綁定。 為了強調這一點(diǎn),這些文章就是按照人們編寫(xiě)代碼的方式寫(xiě)成的。上一篇文章中我給出了一個(gè)簡(jiǎn)單的 XML 模式,并假設有兩個(gè)實(shí)體以此作為相互通信的標準。這兩個(gè)實(shí)體可以是公司、同一公司內的不同部門(mén),也可以是兩個(gè)應用程序組件。無(wú)論哪種情況,都使用 JaxMe 從該模式生成類(lèi),這些類(lèi)然后大概被交給 Java 開(kāi)發(fā)人員。通過(guò) XML 模式的這種 Java 表示,就可以將符合那種模式的 XML 文檔轉化到 Java 類(lèi),或者相反。 | 再重復一次 我相信有些讀者一直堅持閱讀本專(zhuān)欄,對于數據綁定是什么的議論聽(tīng)到過(guò)不下十次。但是,每個(gè)月都有不少新手寫(xiě)信告訴我,他們正在努力弄明白這些東西。請原諒我的羅嗦吧,也許您旁邊的那個(gè)人正在學(xué)習呢,多重復一遍說(shuō)不定能讓您的日子好過(guò)一點(diǎn)! | |
趕上進(jìn)度 首先我們來(lái)回顧上一篇文章中用于生成類(lèi)的模式,如清單 1 所示。 清單 1. 用于學(xué)生的 XML Schema <?xml version="1.0" encoding="UTF-8"?> <schema attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://dw.ibm.com/jaxme/student" xml:lang="EN" xmlns:stu="http://dw.ibm.com/jaxme/student" xmlns="http://www.w3.org/2001/XMLSchema" > <element name="students"> <complexType> <sequence> <element name="student" maxOccurs="unbounded" type="stu:Student" /> <element name="college" minOccurs="0" maxOccurs="unbounded" type="stu:College" /> </sequence> </complexType> </element> <complexType name="Student"> <sequence> <element name="firstName" type="string" /> <element name="lastName" type="string" /> <element name="collegeId" type="string" /> <element maxOccurs="unbounded" name="address" type="stu:Address" /> </sequence> </complexType> <complexType name="Address"> <sequence> <element name="street" type="string" /> <element name="city" type="string" /> <element name="state" type="string" /> <element name="zip" type="positiveInteger" /> </sequence> <attribute name="type" type="string" use="required" /> </complexType> <complexType name="College"> <sequence> <element name="name" type="string" /> <element name="address" type="stu:Address" /> </sequence> <attribute name="id" type="string" use="required" /> </complexType> </schema> | 有了這個(gè)模式之后就可以處理它的實(shí)例文檔,如清單 2 所示。 清單 2. 基本的學(xué)生列表 <?xml version="1.0" encoding="UTF-8"?> <students xmlns="http://dw.ibm.com/jaxme/student" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://dw.ibm.com/jaxme/student student.xsd" > <student> <firstName>Brett</firstName> <lastName>McLaughlin</lastName> <collegeId>LBU</collegeId> <address type="home"> <street>1029 Burlingham</street> <city>Waco</city> <state>TX</state> <zip>87610</zip> </address> </student> <student> <firstName>Gary</firstName> <lastName>Greathouse</lastName> <collegeId>LBU</collegeId> <address type="home"> <street>9098 Townhall Drive</street> <city>Waco</city> <state>TX</state> <zip>87621</zip> </address> </student> <college id="LBU"> <name>Louisiana Baptist University</name> <address type="home"> <street>6301 Westport Avenue</street> <city>Shreveport</city> <state>LA</state> <zip>71129</zip> </address> </college> </students> | 我分別把這兩個(gè)文件命名為 students.xsd和 student1.xml。本文中將讀取 student1.xml,打印其中的一些信息,增加和改變一些信息,然后將修改的數據序列化為一個(gè)新的文件 student2.xml。任務(wù)非常簡(jiǎn)單,但是涉及到了使用 JaxMe 進(jìn)行基本的數據綁定所需要了解的大部分知識。
把 XML 轉化為 Java 代碼 第一步是把這個(gè) XML 文件轉化為 Java 表示。 上一篇 文章的 com.ibm.dw.jaxme.student 包提供了我們需要的類(lèi)?,F在要做的就是讀入 XML 文件,告訴 JaxMe 用什么類(lèi)表示文件中的對象,讓數據綁定 API 完成它們的工作。清單 3 是一個(gè)完成這項工作的例子,先看一遍,后面有詳細的說(shuō)明。 清單 3. 讀取并打印 student1.xml package com.ibm.dw.jaxme.example; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.PrintStream; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; // JAXB classes import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; // SAX classes import org.xml.sax.InputSource; // Generated classes import com.ibm.dw.jaxme.student.*; public class JaxMeTester { /** Input XML File */ private File inputFile; public JaxMeTester(String inputFilename) { this.inputFile = new File(inputFilename); } public Students readXML() throws IOException, JAXBException { // Get a handle to the input file InputSource source = new InputSource(new FileInputStream(inputFile)); source.setSystemId(inputFile.toURL().toString()); // Parse JAXBContext ctx = JAXBContext.newInstance( "com.ibm.dw.jaxme.student"); Unmarshaller u = ctx.createUnmarshaller(); return (Students)u.unmarshal(source); } public void printStudents(Students students, PrintStream out) throws IOException { // Get a map of college IDs and names Map colleges = new HashMap(); List list = students.getCollege(); for (Iterator i = list.iterator(); i.hasNext(); ) { College college = (College)i.next(); colleges.put(college.getId(), college.getName()); } out.print("\n\n--- Student Listings ---\n\n"); list = students.getStudent(); for (Iterator i = list.iterator(); i.hasNext(); ) { Student student = (Student)i.next(); out.println("Name: " + student.getFirstName() + " " + student.getLastName()); List addresses = student.getAddress(); for (Iterator j = addresses.iterator(); j.hasNext(); ) { Address address = (Address)j.next(); printAddress(address, out); } out.println("College: " + colleges.get(student.getCollegeId())); out.println(); } list = students.getCollege(); for (Iterator i = list.iterator(); i.hasNext(); ) { College college = (College)i.next(); out.println("Name: " + college.getName()); out.println("Address: "); printAddress(college.getAddress(), out); out.println(); } } private void printAddress(Address address, PrintStream out) throws IOException { out.print(" " + address.getStreet() + "\n"); out.print(" " + address.getCity() + ", " + address.getState() + " " + address.getZip() + "\n"); } public static void main(String[] args) { if (args.length < 1) { System.err.println("Incorrect arguments supplied!"); System.err.println("Usage: java com.ibm.dw.jaxme.example.JaxMeTester " + "[input XML filename]"); return; } try { JaxMeTester tester = new JaxMeTester(args[0]); Students students = tester.readXML(); tester.printStudents(students, System.out); } catch (Exception e) { System.err.println("Error occurred: " + e.getMessage()); e.printStackTrace(System.err); } } } | 設置輸入文件 首先要將 XML 輸入文件變?yōu)?JaxMe(以及底層的 SAX 解析器)能夠使用的形式。顯然應該選擇 SAX 的 InputSource 類(lèi),這是文件、流以及您能夠想到的任何東西的統一輸入格式。清單 4 中的內容是從上例中摘出來(lái)的,可以看到 JaxMeTester 所接受的 String 文件名被轉化為 InputSource (包括幾個(gè)中間步驟)。 清單 4. 將輸入文件轉化為 InputSource /** Input XML File */ private File inputFile; public JaxMeTester(String inputFilename) { this.inputFile = new File(inputFilename); } public Students readXML() throws IOException, JAXBException { // Get a handle to the input file InputSource source = new InputSource(new FileInputStream(inputFile)); source.setSystemId(inputFile.toURL().toString()); // Parse } | 如果您恰好熟悉 SAX,沒(méi)有什么特別值得注意的地方。惟一需要指出的是 File 的使用,這里沒(méi)有直接傳遞 String 。雖然可以采用后一種方法,但是這樣做就沒(méi)有 Java 語(yǔ)言 File 類(lèi)所提供的保護了。事實(shí)上, InputSource 在構造函數中做的第一件事就是將 String 轉化為 File ,這正是我們要做的。此外,這種方法很容易設置輸入文件的系統 ID,如果直接使用 String 而不是對象就麻煩得多了。 設置 JaxMe 接下來(lái)要設置 JaxMe 解組,只需要一行代碼,如清單 5 所示。這里要告訴 JAXB 上下文(要記住 JaxMe 是 JAXB 的一種實(shí)現,因此常常使用這種語(yǔ)義)到哪里尋找 jaxb.properties文件。我總是將其放在與相關(guān)類(lèi)相同的目錄中,就是說(shuō)只要使用生成類(lèi)的包名就可以了。 清單 5. 告訴 JaxMe 到哪里尋找屬性文件 JAXBContext ctx = JAXBContext.newInstance( "com.ibm.dw.jaxme.student"); | 該文件本身只有一行,如清單 6 所示,它告訴 JAXB 要加載哪一種數據綁定上下文實(shí)現。 清單 6. jaxb.properties 文件 javax.xml.bind.context.factory=org.apache.ws.jaxme.impl.JAXBContextImpl | 這里值得一提的是代碼中 沒(méi)有JaxMe 專(zhuān)用的類(lèi)。雖然必須將 JaxMe 放在類(lèi)路徑中,但 JAXB 從這個(gè)屬性文件中獲得所有 JaxMe 專(zhuān)用的信息。換句話(huà)說(shuō),不用改變代碼就可以從 JAXB 的參考實(shí)現切換為 JaxMe(強烈建議使用)。 解組 建立了 JAXB 上下文之后,將 XML 文件轉化為 Java 表示很容易,如清單 7 所示,這些細節沒(méi)有吸引人的地方。 清單 7. 解組 XML public Students readXML() throws IOException, JAXBException { // Get a handle to the input file InputSource source = new InputSource(new FileInputStream(inputFile)); source.setSystemId(inputFile.toURL().toString()); // Parse JAXBContext ctx = JAXBContext.newInstance( "com.ibm.dw.jaxme.student"); Unmarshaller u = ctx.createUnmarshaller(); return (Students)u.unmarshal(source); } | 當然,這段代碼非常令人厭煩,但正因如此也就顯得很棒;這僅僅是 勞動(dòng),就您而言不用花費多少心思。 處理 XML 一旦獲得 Students 對象,也就完成了這個(gè)練習中的數據綁定部分。 printStudents() 方法可以說(shuō)明這一點(diǎn),因為它根本不知道 JAXB 或者 JaxMe 的存在。事實(shí)上也可以在不同的類(lèi)中(甚至非 Java 語(yǔ)言模塊中),沒(méi)有任何問(wèn)題。這里不再重復列出代碼,只不過(guò)是 Java 對象的一些打印調用。 程序的輸出 您可以運行 JaxMeTester 并提供前面的 XML 輸入文件來(lái)測試這些代碼。該程序將會(huì )折騰上一秒鐘,然后輸出與清單 8 類(lèi)似的結果。這是最漂亮的打印工作,但是應該讓您明白使用 JaxMe 讀 XML 文件是多么簡(jiǎn)單。 清單 8. 程序對 student1.xml 的輸出結果 test: [java] --- Student Listings --- [java] Name: Brett McLaughlin [java] 1029 Burlingham [java] Waco, TX 87610 [java] College: Louisiana Baptist University [java] Name: Gary Greathouse [java] 9098 Townhall Drive [java] Waco, TX 87621 [java] College: Louisiana Baptist University [java] Name: Louisiana Baptist University [java] Address: [java] 6301 Westport Avenue [java] Shreveport, LA 71129 BUILD SUCCESSFUL Total time: 3 seconds | | 轉向 Ant 與以前的文章一樣,我使用 Ant 完成編譯、運行和其他大部分工作。上一篇文章中已經(jīng)詳細介紹了 Ant 的用法,因此這里只需要引入構建文件( build.xml)就可以了。默認的目標包括生成類(lèi)、編譯生成的示例類(lèi)并運行該例子,因此您只需要修改幾個(gè)路徑并輸入 ant 就可以了。 | |
使用數據 您可能已經(jīng)猜到,一旦轉化成 Java 形式這些數據的使用就非常簡(jiǎn)單了。而且這些操作同樣與 JaxMe 毫無(wú)關(guān)系。因此我只給出一些代碼,這些代碼增加一所新的學(xué)院并改變一直處理的學(xué)校,代碼的功能您可以自己分析,新增的方法如清單 9 所示。 清單 9. 在內存中修改學(xué)生信息 public void modifyStudents(Students students) { // Add a college College college = new com.ibm.dw.jaxme.student.impl.CollegeImpl(); college.setName("Norris Bible Baptist Seminary"); college.setId("NBBS"); Address address = new com.ibm.dw.jaxme.student.impl.AddressImpl(); address.setStreet("724 North Jim Wright Freeway"); address.setCity("Ft. Worth"); address.setState("TX"); address.setZip(new java.math.BigInteger("76108")); college.setAddress(address); // Add the college in List colleges = students.getCollege(); colleges.add(college); // Change a student‘s college List list = students.getStudent(); for (Iterator i = list.iterator(); i.hasNext(); ) { Student student = (Student)i.next(); if (student.getFirstName().equals("Brett") && student.getLastName().equals("McLaughlin")) { student.setCollegeId("NBBS"); } } } | 代碼主體中還增加了一些額外的打印語(yǔ)句,如清單 10 所示。 清單 10. 其他的打印語(yǔ)句 public static void main(String[] args) { if (args.length < 1) { System.err.println("Incorrect arguments supplied!"); System.err.println("Usage: java com.ibm.dw.jaxme.example.JaxMeTester " + "[input XML filename]"); return; } try { JaxMeTester tester = new JaxMeTester(args[0]); Students students = tester.readXML(); System.out.println("Students after reading in from disk..."); tester.printStudents(students, System.out); tester.modifyStudents(students); System.out.println("\n\nStudents after in-memory modifications..."); tester.printStudents(students, System.out); } catch (Exception e) { System.err.println("Error occurred: " + e.getMessage()); e.printStackTrace(System.err); } } | 輸出結果如清單 11 所示,其中僅列出了學(xué)生清單修改 后的結果。 清單 11. 修改后的打印輸出 [java] --- Student Listings --- [java] Name: Brett McLaughlin [java] 1029 Burlingham [java] Waco, TX 87610 [java] College: Norris Bible Baptist Seminary [java] Name: Gary Greathouse [java] 9098 Townhall Drive [java] Waco, TX 87621 [java] College: Louisiana Baptist University [java] Name: Louisiana Baptist University [java] Address: [java] 6301 Westport Avenue [java] Shreveport, LA 71129 [java] Name: Norris Bible Baptist Seminary [java] Address: [java] 724 North Jim Wright Freeway [java] Ft. Worth, TX 76108 | 對于多數讀者而言這都是些老生常談,但對于剛接觸數據綁定的讀者而言,讓我強調一下這一小段代碼的重要意義。它說(shuō)明您不需要 將 XML 作為 XML處理。事實(shí)上,除了將您帶入數據綁定的大門(mén)之外,Java 代碼一直在完成其他所有工作。雖然 SAX 和 DOM(以及 JDOM、dom4j 等等)很重要,而且對于底層系統可以說(shuō)至關(guān)重要,但一般的 Java 程序員不再需要了解這些東西了。 Java 程序員可以編寫(xiě)接收和輸出基本 Java 對象的所有方法,不論這些對象來(lái)自何處去向何方。就像良好的數據庫代碼把數據庫交互和普通程序員分隔開(kāi)一樣,數據綁定也能做到。一旦某個(gè)方法不再使用對象,它就不需要知道信息是否被保存,當然也不需要知道信息是 如何保存的。這正是數據綁定的優(yōu)美之處!
從 Java 轉化到 XML 現在要將修改后的列表再保存到 XML 中。雖然可以覆蓋原來(lái)的 student1.xml 文件,但是我更喜歡寫(xiě)入一個(gè)新的文件(從而可以比較異同)student2.xml。為此需要稍微修改 main() 方法,如清單 12 所示。 清單 12. 增加第二個(gè)參數作為輸出文件名 public static void main(String[] args) { if ( args.length < 2) { System.err.println("Incorrect arguments supplied!"); System.err.println("Usage: java com.ibm.dw.jaxme.example.JaxMeTester " + "[input XML filename] [output XML filename]"); return; } try { JaxMeTester tester = new JaxMeTester(args[0]); Students students = tester.readXML(); System.out.println("Students after reading in from disk..."); tester.printStudents(students, System.out); tester.modifyStudents(students); System.out.println("\n\nStudents after in-memory modifications..."); tester.printStudents(students, System.out); tester.writeStudents(students, args[1]); } catch (Exception e) { System.err.println("Error occurred: " + e.getMessage()); e.printStackTrace(System.err); } } | 增加序列化代碼 現在剩下的只有新的 writeStudents() 方法了,這個(gè)方法如此簡(jiǎn)單,我找不到任何理由來(lái)進(jìn)一步解釋?zhuān)缜鍐?13 所示。 清單 13. 序列化 XML public void writeStudents(Students students, String outputFile) throws IOException, JAXBException { // Serialize JAXBContext ctx = JAXBContext.newInstance( "com.ibm.dw.jaxme.student"); Marshaller m = ctx.createMarshaller(); FileWriter writer = new FileWriter(outputFile); m.marshal(students, writer); writer.close(); } | | 丟失的 import 語(yǔ)句 現在還需要增加幾個(gè) import 語(yǔ)句。我不準備列出整個(gè)文件,但建議您下載本文的代碼,其中包括完整的 JaxMeTester 源代碼。 | | 這些代碼看起來(lái)與解組過(guò)程非常相似。通過(guò) JAXB 創(chuàng )建了一個(gè)新的 Marshaller ,同樣使用指定位置的 jaxb.properties 文件。然后將輸出文件名包裝在一個(gè) writer 中,執行序列化并關(guān)閉 writer( 千萬(wàn)不要忘記關(guān)閉 writer?。?。嗚啦!編碼、編譯然后運行。 還記得“往返”嗎? 結束之前讓我們看一看輸出文件(如果您使用了我給出的名稱(chēng)應該是 student2.xml),如清單 14 所示。 清單 14. student2.xml <stu:students xmlns:stu="http://dw.ibm.com/jaxme/student"> <stu:student> <stu:firstName>Brett</stu:firstName> <stu:lastName>McLaughlin</stu:lastName> <stu:collegeId>NBBS</stu:collegeId> <stu:address type="home"> <stu:street>1029 Burlingham</stu:street> <stu:city>Waco</stu:city> <stu:state>TX</stu:state> <stu:zip>87610</stu:zip> </stu:address> </stu:student> <stu:student> <stu:firstName>Gary</stu:firstName> <stu:lastName>Greathouse</stu:lastName> <stu:collegeId>LBU</stu:collegeId> <stu:address type="home"> <stu:street>9098 Townhall Drive</stu:street> <stu:city>Waco</stu:city> <stu:state>TX</stu:state> <stu:zip>87621</stu:zip> </stu:address> </stu:student> <stu:college id="LBU"> <stu:name>Louisiana Baptist University</stu:name> <stu:address type="home"> <stu:street>6301 Westport Avenue</stu:street> <stu:city>Shreveport</stu:city> <stu:state>LA</stu:state> <stu:zip>71129</stu:zip> </stu:address> </stu:college> <stu:college id="NBBS"> <stu:name>Norris Bible Baptist Seminary</stu:name> <stu:address> <stu:street>724 North Jim Wright Freeway</stu:street> <stu:city>Ft. Worth</stu:city> <stu:state>TX</stu:state> <stu:zip>76108</stu:zip> </stu:address> </stu:college> </stu:students> | 您馬上就會(huì )注意到所有的元素都正確使用了名稱(chēng)空間,而這個(gè)名稱(chēng)空間用前綴 stu 給出。因此該文件在語(yǔ)義上與輸入是等價(jià)的(當然包含了新增加的信息),雖然看起來(lái)非常不同。這需要回顧 本系列文章的第一篇中所討論的問(wèn)題 —— 往返,XML 允許這樣做,如果不習慣的話(huà)可能會(huì )令您感到困惑。如果您完全迷惑了,請再讀一讀第一篇文章。但是在明確指出這種差別之前,我還不想結束本文。
再論工具 API 還 記得前面對工具 API 的討論嗎?我曾經(jīng)說(shuō)過(guò)數據綁定和工具 API 不同。我希望您注意到了這句話(huà),因為這一點(diǎn)非常重要。數據綁定 API(JaxMe 僅是其中之一)的不利之處是很容易彌漫到所有的代碼中。我曾經(jīng)見(jiàn)過(guò)一些項目,雖然采用的體系結構相對不錯,但是數據綁定出現在所有能夠想像得到的地方,并 且有幾個(gè)地方我 從來(lái)都沒(méi)有想像過(guò)。結果如果需要修改代碼,升級和 API 轉換是完全不可能的,因為應用程序的很多部分必須重新實(shí)現、重新測試、重新部署。 作為一名程序員應盡量避免出現這種情況。按照經(jīng)驗法則,應用程序層次之間應保持 無(wú)關(guān)性而非 依賴(lài)性。 為此,應使用數據綁定 API 隔離業(yè)務(wù)層和編組解組 XML 的代碼之間的交互??梢栽黾右粋€(gè)數據綁定層,防止直接調用 JaxMe(或者 JAXB 以及所用的其他 API)。我曾經(jīng)看到過(guò)各種各樣的解決方案 —— 我自己也曾提出幾種方法 —— 只要您愿意隔離這些代碼。這意味著(zhù)升級或者修改只影響到隔離的少量代碼,應用程序的其他部分仍然可以運行。記住這一點(diǎn),您就不會(huì )煩惱纏身了。
結束語(yǔ) 掌握了 JaxMe 的基本用法之后,下一篇文章將介紹該 API 較難的地方,分析 JaxMe 為什么比它的表兄弟 JAXB 提供了 更多的功能。具體來(lái)說(shuō),我將關(guān)注數據庫支持,詳細探討如何使用 JaxMe 向數據庫中插入數據,如 MySQL。然后如果時(shí)間和空間允許的話(huà)(只能希望),我還將說(shuō)明同樣的技術(shù)如何用于 XML 數據庫。 再后面呢?只有我的想像力才知道。:)我確實(shí)有一些想法,不過(guò)您要堅持讀下去才會(huì )知道到底是什么。到那時(shí)候希望能再看到您。
參考資料
關(guān)于作者 | | | | Brett McLaughlin 從 Logo 時(shí)代(還記得那個(gè)小三角嗎?)就開(kāi)始從事計算機。最近幾年,他已經(jīng)成為 Java 技術(shù)和 XML 社區最知名的作家和程序員之一。他曾經(jīng)在 Nextel Communications 實(shí)現過(guò)復雜的企業(yè)系統,在 Lutris Technologies 實(shí)際編寫(xiě)應用程序服務(wù)器,最近又在 O‘Reilly Media, Inc. 繼續撰寫(xiě)和編輯這方面的書(shū)籍。他的新著(zhù) Java 1.5 Tiger: A Developer‘s Notebook是關(guān)于新版本 Java 技術(shù)的第一本參考書(shū),經(jīng)典巨著(zhù) Java and XML仍然是在 Java 技術(shù)中使用 XML 技術(shù)的權威參考。 | |