數據綁定系列的第二篇是如何從 XML 數據限制中生成一個 Java 語言。 本文通過完整的代碼展現了如何生成類和代碼,並提供了如何定制您自己版本的建議。 還沒有看過第一篇嗎?第一篇, "對象,無處不在的對象", 解釋了數據綁定是如何將 XML 和 Java 語言對象互為轉換。它比較了數據綁定和其它在 Java 程序中處理 XML 的方法, 並介紹了一個 XML 配置文檔示例。第一部分也介紹了使用 XML Schema 來約束數據。
在深入 Java 程序和 XML 代碼之前,先快速回顧一下本系列第一部分所打下的基礎。
在第一部分中,我們知道只要可以標識文檔的一組約束,就可以將文檔轉換成 Java 對象。那些約束為數據提供了接口。如 Web 服務配置文檔示例中所示,XML 文檔應當成為現有 Java 類的一個實例,並且從數據約束生成那個類。最後,會看到表示樣本 XML 文檔約束的 XML schema。
如果對細節還有疑問,請回顧 第一篇文章.
打造基礎
現在,可以著手從 XML schema 創建 Java 類。該類必須准確表示數據約束,並提供 Java 應用程序將使用的簡單讀方法和寫方法。開始之前,讓我們先回顧清單 1,查看為 WebServiceConfiguration 文檔定義的 XML schema。
清單 1. 表示 Web 容器配置文檔數據接口的 XML schema
<?xml version="1.0"?>
<schema targetNamespace="http://www.enhydra.org"
xmlns="http://www.w3.org/1999/xmlSchema"
xmlns:enhydra="http://www.enhydra.org"
>
<complexType name="ServiceConfiguration">
<attribute name="name" type="string" />
<attribute name="version" type="float" />
</complexType>
<element name="serviceConfiguration" type="ServiceConfiguration" />
<complexType name="WebServiceConfiguration"
baseType="ServiceConfiguration"
derivedBy="extension">
<element name="port">
<complexType>
<attribute name="protocol" type="string" />
<attribute name="number" type="integer" />
<attribute name="protected" type="string" />
</complexType>
</element>
<element name="document">
<complexType>
<attribute name="root" type="string" />
<attribute name="index" type="string" />
<attribute name="error" type="string" />
</complexType>
</element>
</complexType>
<element name="webServiceConfiguration" type="WebServiceConfiguration" />
</schema>
生成代碼
開始生成 Java 代碼之前,首先必須確定核心類的名稱。將使用 org.enhydra.xml.binding 包中的 SchemaMapper,它是 Enhydra 應用服務器實用程序類集合的一部分。還可以將任何必需的支持類放到這個包中。
除了類名稱以外,還必須確定用來讀取和創建 XML 的 Java API。如上一篇文章中所討論過的,三種主要選擇是 SAX、DOM 和 JDOM。由於 SAX 僅僅適用於讀取 XML 文檔,因此它不適合創建 XML。由於在打包階段中要將 Java 對象轉換為 XML 表示,因此在此階段中需要創建 XML。這就將選擇的范圍縮小到 DOM 和 JDOM。在這兩種選擇都可用的情況下,本例中我選擇使用 JDOM API,僅為了顯示其功能性(並不僅僅因為我是它的合著者之一!)。
最後,必須指出如何將 XML schema 提供給 SchemaMapper 類。通常,可以假設類的生成是脫機完成的(通過靜態 main 方法)。僅通過使 main 方法調用非靜態方法,還可以從運行時環境中使用類。做了這些決定後,就可以開始勾畫類的框架了。
組裝 SchemaMapper 類框架
要做的第一件事就是為要生成的代碼設置一些基本存儲器。必須能夠從每個執行映射的 XML schema 生成多個接口和實現。Java HashMap 正好滿足要求。鍵是接口或實現名稱以及映射表中的值,該值是將要輸出到新 Java 程序文件的實際代碼。還需要存儲每對接口/實現的屬性(屬性是在這兩種類之間共享的)。這裡,我再次使用 HashMap。其中,鍵是接口名稱。但是,由於每個接口可能有多個屬性,因此該值是另一個具有屬性及其類型的 HashMap。最後,必須存儲 XML schema 的名稱空間,因為 JDOM 將使用這個名稱空間來訪問 XML schema 中的結構。所有這些具體信息都足以初步勾畫出新類的框架,新類在清單 2 中。
還請注意在清單 2 中已添加了兩個需要使用的基本方法:其中一個方法需要使用 XML schema 的 URL 來執行生成(允許它在網絡可訪問 schema 以及本地 schema 下運行),另一個方法將類輸出到指定的目錄中。最後,簡單的 main 方法將 XML schema 看作一個變量,然後執行生成。
清單 2. SchemaMapper 類的框架
package org.enhydra.xml.binding;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Iterator;
import java.util.List;
// JDOM classes used for document representation
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.NoSuchAttributeException;
import org.jdom.NoSuchChildException;
import org.jdom.input.SAXBuilder;
/**
<p>
<code>SchemaMapper</code> handles generation of Java interfaces and classes
from an XML schema, essentially allowing data contracts to be set up
for the binding of XML instance documents to Java objects.
</p>
@author Brett McLaughlin
/
public class SchemaMapper {
/** Storage for code for interfaces */
private Map interfaces;
/** Storage for code for implementations */
private Map implementations;
/** Properties that accessor/mutators should be created for */
protected Map properties;
/** XML Schema Namespace */
private Namespace schemaNamespace;
/** XML Schema Namespace URI */
private static final String SCHEMA_NAMESPACE_URI =
"http://www.w3.org/1999/xmlSchema";
/**
* <p>
*Allocate storage and set up defaults.
* </p>
*/
public SchemaMapper() {
interfaces = new HashMap();
implementations = new HashMap();
properties = new HashMap();
schemaNamespace = Namespace.getNamespace(SCHEMA_NAMESPACE_URI);
}
/**
* <p>
*This is the "entry point" for generation of Java classes from an XML
*Schema. It allows a schema to be supplied, via <code>URL</code>,
*and that schema is used for input to generation.
* </p>
*
* @param schemaURL <code>URL</code> at which XML Schema is located.
* @throws <code>IOException</code> - when problems in generation occur.
*/
public void generateClasses(URL schemaURL) throws IOException {
// Perform generation
}
/**
* <p>
*This will write out the generated classes to the supplied stream.
* </p>
*
* @param directory <code>File</code> to write to (should be a directory).
* @throws <code>IOException</code> - when output errors occur.
*/
public void writeClasses(File dir) throws IOException {
// Perform output to files
}
/**
* <p>
*This provides a static entry point for class generation from
*XML Schemas.
* </p>
*
* @param args <code>String[]</code> list of files to parse.
*/
public static void main(String[] args) {
SchemaMapper mapper = new SchemaMapper();
try {
for (int i=0; i<args.length; i++) {
File file = new File(args[i]);
mapper.generateClasses(file.toURL());
mapper.writeClasses(new File("."));
}
} catch (FileNotFoundException e) {
System.out.println("Could not locate XML Schema: ");
e.printStackTrace();
} catch (IOException e) {
System.out.println("Java class generation failed: ");
e.printStackTrace();
}
}
}
In 清單 2中,可以看到對於每個作為自變量傳遞的 XML schema,main 方法都調用生成過程。首先,方法會生成類。將文件名轉換為 URL,並傳遞到 generateClasses(URL schemaURL) 。然後,通過 writeClasses(File dir) 方法將類寫到當前目錄中(轉換成 Java File: new File("."))。
任何其它 Java 類都可以在運行時進行相同的調用,並生成類。例如,一個定制類裝入器也許能發現需要打包,確定仍要生成的接口和實現,並使用 SchemaMapper 類來執行該任務。所有這一切都在運行時完成。因為 generateClasses() 方法需要一個 URL,所以在網絡上使用這個類非常簡單。例如,可以使用它來請求從 HTTP 上公開可用的 XML schema 生成類。
由於對如何使用類做了盡量少的假設,因此它是一個普通類;程序可以同時在本地和遠程使用它。並且這個類可以當作一組 Java 語言和 XML 實用程序類的一部分,而不是必須以某種特殊形式使用的專用類。這種可重用性原則對 XML 特別關鍵,因為在不同系統上進行網絡訪問和通信是 XML 的基本前提。
生成類
構建好類的框架後,就可以添加類的主體了。
我已經提到過生成過程具有遞歸性質。請記住這一點,需要填充 generateClasses() 方法才能開始。可以使用 JDOM 讀取 XML schema,然後從 schema 中抽取每個 complexType 元素。對於這些元素中的每一個,如清單 3 所示,遞歸進程從 handleComplexType() 調用處開始(以後將進一步討論)。
清單 3. The generateClasses() 方法
public void generateClasses(URL schemaURL) throws IOException {
/**
* Create builder to generate JDOM representation of XML Schema,
* without validation and using Apache Xerces.
*/
SAXBuilder builder = new SAXBuilder();
try {
Document schemaDoc = builder.build(schemaURL);
// Handle complex types
List complexTypes = schemaDoc.getRootElement()
.getChildren("complexType",
schemaNamespace);
for (Iterator i = complexTypes.iterator(); i.hasNext(); ) {
// Iterate and handle
Element complexType = (Element)i.next();
handleComplexType(complexType);
}
} catch (JDOMException e) {
throw new IOException(e.getMessage());
}
}
為簡便起見,將強調一些重點,而不是詳細闡述將 schema 轉換為 Java 類的整個過程。可以 聯機查看完整的 SchemaMapper 類,或者可以 下載它。
生成器必須確定在 XML schema 中找到的每個 complexType 元素是 顯式的(具有“類型”屬性),還是 隱式的(沒有“類型”屬性)。如果類型是顯式的,則類型將成為接口名稱,並且首字母大寫。如果類型是隱式的,那麼將根據特性名稱構造接口名稱。清單 4 中顯示了處理這個邏輯的代碼段。(如要了解更多數據綁定的定義,請參閱側欄, 術語解釋。)
清單 4. 確定接口名稱
// Determine if this is an explict or implicit type
String type = null;
// Handle extension, if needed
String baseType = null;
try {
// Assume that we are dealing with an explicit type
type = complexType.getAttribute("name")
.getValue();
} catch (NoSuchAttributeException e) {
/*
* It is safe with an implicit type to assume that the parent
* is of type "element", has no "type" attribute, and that we
* can derive the type as the value of the element's "name"
* attribute with the word "Type" appended to it.
*/
try {
type = new StringBuffer()
.append(BindingUtils.initialCaps(
complexType.getParent()
.getAttribute("name")
.getValue()))
.append("Type")
.toString();
} catch (NoSuchAttributeException nsae) {
// Shouldn't happen in schema-valid documents
throw new IOException("All elements must at have a name.");
}
}
因此,根據代碼中的命名約定, 具有ServiceConfiguration 類型的元素將生成名為 ServiceConfiguration 的 Java 接口。名為 port 但 沒有顯式類型的元素將生成叫做 PortType 的 Java 接口。它采用元素名稱 ( port ),將首字母轉成大寫 ( Port ),再加上單詞 Type ,就得到了 PortType 。
同樣,所有實現類都使用接口名稱,然後添加縮寫 Impl 。所以,最終實現類是 ServiceConfigurationImpl 和 PortTypeImpl 。
使用這些命名約定,您可以很容易地確定將數據約束映射到 Java 接口會得到哪些 Java 類。如果設置了應用程序在運行時裝入類,那麼類裝入器或其它實用程序可以迅速確定是否已裝入了所需的類。類裝入器或實用程序只要從 XML schema 中找出生成的類名稱,然後嘗試裝入它們就可以了。命名邏輯是事先確定的,因此檢查起來非常方便。
一旦確定了名稱,就可以生成接口和實現類的框架(請參閱清單 5)。
清單 5. 生成代碼
StringBuffer interfaceCode = new StringBuffer();
StringBuffer implementationCode = new StringBuffer();
/*
* Start writing out the interface and implementation class
* definitions.
*/
interfaceCode.append("public interface ")
.append(interfaceName);
// Add in extension if appropriate
if (baseType != null) {
interfaceCode.append(" extends ")
.append(baseType);
}
interfaceCode.append(" {\n");
implementationCode.append("public class ")
.append(implementationName);
// Add in extension if appropriate
if (baseType != null) {
implementationCode.append(" extends ")
.append(baseType)
.append("Impl");
}
implementationCode.append(" implements ")
.append(interfaceName)
.append(" {\n");
// Add in properties and methods
// Close up interface and implementation classes
interfaceCode.append("}");
implementationCode.append("}");
實際上,生成屬性和方法是相當簡單的。將接口和相應實現的名稱添加到類的存儲器中,然後是右花括號,它們的作用是結束類。像這樣成對生成類,而不是單獨生成類,將使同時在接口和實現反映出該過程變得簡單。檢查源代碼(請參閱 參考資料),就可以得到足夠的解釋。
清單 5中的粗體注釋表示源列表中的多行代碼。在這裡精簡代碼是為了保持簡潔。對於正在創建的 XML schema 的每個特性(由 schema attribute 表示),都會將讀方法和寫方法添加到接口和實現(實現還有執行方法邏輯的代碼)。同時,將為實現類的代碼添加變量。
最終結果就是本系列第一部分中生成的類。可以在這裡查看它們,或者與本文中的其余代碼一起下載(請參閱 參考資料):
ServiceConfiguration.java
ServiceConfigurationImpl.java
PortType.java
PortTypeImpl.java
DocumentType.java
DocumentTypeImpl.java
WebServiceConfiguration.java
WebServiceConfigurationImpl.java
有兩個輔助程序類也將參與類生成:
BindingUtils ,將首字母變成大寫。雖然,可以將這個方法添加到生成器類,但我打算以後在打包和解包類時再使用該方法,所以我將它歸到這個輔助程序類中。可以 聯機查看 BindingUtils ,或者可以 下載它。
DataMapping , SchemaMapper 類用來轉換數據類型。可以 聯機查看源碼或者 下載源碼。
完成包
如許多其它開放源碼軟件,在這裡顯示的數據綁定包是一項正在進行中的工作。雖然它已經初具規模,但仍有很大空間可用於添加更多功能和做改進。因此,以這段代碼為基礎,可以有許多方式應用程序中加以衍生。
可以重新使用該樣本代碼,以將 XML schema 的數據約束轉換為類型安全的 Java 接口和實現。例如,迄今為止,示例代碼還沒有處理 XML schema 中可能指定的范圍。而對於許多 XML 開發人員,那些數據范圍才是使用 schema 的真正原因。然後,請考慮清單 6 中 Web 服務的擴充 XML schema。
清單 6. 帶擴充約束的 Web 服務配置
<?xml version="1.0"?>
<schema targetNamespace="http://www.enhydra.org"
xmlns="http://www.w3.org/1999/xmlSchema"
xmlns:enhydra="http://www.enhydra.org"
>
<complexType name="ServiceConfiguration">
<attribute name="name" type="string" />
<attribute name="version" type="float" />
</complexType>
<element name="serviceConfiguration" type="ServiceConfiguration" />
<complexType name="WebServiceConfiguration"
baseType="ServiceConfiguration"
derivedBy="extension">
<element name="port">
<complexType>
<attribute name="protocol" type="string" />
<attribute name="number">
<simpleType base="integer">
<minExclusive value="0" />
<maxInclusive value="32767" />
</simpleType>
</attribute>
<attribute name="protected" type="string" />
</complexType>
</element>
<element name="document">
<complexType>
<attribute name="root" type="string" />
<attribute name="index" type="string" />
<attribute name="error" type="string" />
</complexType>
</element>
</complexType>
<element name="webServiceConfiguration" type="WebServiceConfiguration" />
</schema>
清單 6說明了number屬性的類型, 並且在用紅色強調的幾行中指定了值的合法范圍(1 到 32,767)。當前版本的 SchemaMapper 將忽略這些附加聲明。從 schema 創建 Java 接口和實現類時,沒有必要處理 XML schema 中的 minXXX 和 maxXXX 關鍵字,但它們可以增加相當多的附加驗證。
請查看清單 7 中的代碼示例,這些代碼是可在實現類中生成的代碼,以確保只有 schema 指定范圍中的值可以作為變量。
清單 7. 帶范圍檢查的生成代碼
public class PortTypeImpl implements PortType {
private String protocol;
private int number;
private String protected;
public void setNumber(int number) {
if ((number > 0) && (number <= 32767)) {
this.number = number;
} else {
throw IllegalArgumentException("Argument must be greater than 0
and less than or equal to 32767");
}
}
public int getNumber() {
return number;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public String getProtocol() {
return protocol;
}
public void setProtected(String protected) {
this.protected = protected;
}
public String getProtected() {
return protected;
}
}
如果對類提供了非法值,那麼清單 7 中的生成代碼塊將拋出一個運行時異常,這樣既確保了類型安全性又確保了范圍安全性。
可以很方便地將類似於清單 6 和清單 7 中的增強部分添加到我提供的基本代碼中,因為本文中的所有代碼完全都是開放源碼。您也許還想加入 Enhydra 體系結構工作組郵件發送清單,在該清單中維護和討論了該代碼的未來版本和修訂本。可以從 Enhydra Web 站點上加入該清單,列在本文的 參考資料中。
總結
目前為止,應該已經了解什麼是數據綁定。已知道使用數據綁定的原因,特別是配置信息。已經掌握如何創建 XML schema 和配置 Web 容器服務的 XML 實例文檔,而且我們已經詳細討論了 org.enhydra.xml.binding.SchemaMapper 類。使用這個類,您可以創建 Java 接口和(該接口的)實現,它將管理從 XML 文檔創建的 Java 實例。還知道如何將約束從 XML schema 映射到 Java。
現在,已經可以進入下一部分。在下一部分中,將開始把 XML 文檔實際轉換為 Java 對象的過程,其中 Java 對象是生成類的實例。下一篇文章將說明如何完成這個過程,及其逆向過程,以及 org.enhydra.xml.binding.Unmarshaller 和 org.enhydra.xml.binding.Marshaller 類。這兩個類將磁盤上文本的 XML 格式數據移到內存中的 Java 表示,然後再移回來。
希望您能喜歡 XML schema 生成類,下次再見!
以上所有源碼均附在文檔開始處的源代碼下載鏈接中。
本文配套源碼