从XML到Java代码的数据绑定之二

类别:Java 点击:0 评论:0 推荐:
本文转自 IBM developerWorks 中国网站

从 XML 数据创建类

  数据绑定系列的第二篇是如何从 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 表示,然后再移回来。


本文地址:http://com.8s8s.com/it/it15851.htm