使用 XML Parser API — JSR 172

作者:Vikram Goyal

本文介绍 XML 分析 API 的基础知识,并使用事件驱动的分析方法 SAX(XML 简单 API)作为示例。

2011 年 11 月发布
 

下载:

下载Java ME

下载示例代码 (Zip)

简介

JSR 172 已经应用一段时间了。实际上,它现在已作为一个向后兼容的子集包含在 JSR 280 中,后者是一个适用于 Java Platform Micro Edition (Java ME) 设备的万能 XML 处理 API。最初创建该 API 时,它不叫 XML Parser API,而是被称作 J2ME Web Services API。它有两个主要用途:访问基于 XML/SOAP 的远程 Web 服务和分析 XML 数据。

在本文中,我将使用一个具体示例来介绍 XML 分析 API 的基础知识。具体来说,我将介绍 SAX(XML 简单 API)分析方法,因为另一个方法 DOM(文档对象模型)由于占用大量内存(除了原始规范禁止使用之外)已不再流行。

:本文所提供示例的 NetBeans 源代码可从这里下载。

XML 简单 API

2011 年,要想在开发社区中找到一个未使用过 XML 的人会相当困难。到处都在用 XML,它是大多数数字通信的支柱。但不久之前它还是一个新兴的平台,要将其用作数据通信标准,通常认为需要高性能的移动设备。然而,需要数据是一回事;能够将该数据转换成开发环境中有用的模型又是另一回事。

当时(现在仍然是)流行两种分析此数据以便将其转换成编程模型的方法:SAX(XML 简单 API)和 DOM(文档对象模型)。

这两种方法的区别在于处理 XML 数据的方式。DOM 读取整个文档然后在内存中创建一个模型,而 SAX 按顺序处理 XML 数据,同时生成事件供处理程序处理。

此外,SAX 是基于事件的。它不允许在 XML 文档中插入或删除节点,并且在工作时不需要占用很多资源。而 DOM 则不同,它是基于树结构的,允许修改节点,并会占用大量内存。这对现代设备可能不是什么大问题,但在不久以前,对于内存受限的设备,这是一个很大的问题。因此,XML Parser API 不允许在 XML 处理中使用 DOM,并强制只能使用 SAX。

使用 SAX

因为 SAX 是事件驱动的分析器,它提供列出的各种事件,这些事件触发对特定方法的回调。以下是一些与这些事件关联的方法:

  • startDocument 和 endDocument:在开始和完成文档遍历时触发
  • startElement 和 endElement:在分析器遇到 XML 元素的开始和结束标记时触发
  • characters:在元素内的数据处理完成时触发

XML Parser API 提供了一个 DefaultHandler 类。如果您作为开发人员希望为特定 XML 文档创建分析器,需要扩展此类并为列出的方法(只有您认为可能需要的方法)提供实际代码。然后这些方法必须基于分析文档时提供的数据创建一个模型。这些方法还必须验证数据并相应地报告所有错误。

XML Parser API 为创建 SAX 分析器提供了一个标准工厂。您为处理自己的 XML 文档创建的处理程序必须与被分析的 XML 文档一起提供给此分析器实例,例如:

 parser = 
   SAXParserFactory.newInstance().
   newSAXParser();
   parser.parse(is, saxMenuHandler);
 

简单来讲,为了分析自定义 XML 文档,需要创建一个处理程序来扩展 API 提供的 DefaultHandler 类。这个自定义处理程序负责监听来自分析器的事件并基于这些事件(以及提供的数据)创建模型。您的处理程序负责验证文档及其数据。我们来看看此理论是如何付诸实践的。

工作示例

在我们的工作示例中,我将创建一个处理程序来分析以下示例 XML 文档:

 <?xml version="1.0" encoding="UTF-8"?>
<menu restaurantName="My Modern Restaurant" phone="555-555-5555">
     <entree veg="N">
       <name>Freshly Shucked Oysters</name>
       <price>5.00</price>    
     </entree>
     <entree veg="N">
       <name>Duck Consomme with mushrooms</name>
       <price>7.00</price>    
     </entree>
     
     <main veg="Y">
       <name>Eggplant Mozzarella</name>
       <price>25.00</price>
       <description>Served with our delicious sauce</description>    
     </main>
     <main veg="N">
       <name>King Salmon</name>  
       <price>32.50</price>
       <description>Served with potato salad and quail egg</description>
     </main>      
   </menu>

这个示例很普通;只是一个假想餐厅的菜单。

创建模型对象

开始编写分析器代码之前,首先创建表示这些数据的模型对象。需要三个模型:Menu、Entrée 和 Main。以下是 model/Menu.java:

 model/Menu.java
   package model;
   import java.util.Vector;
public class Menu {
     private Vector entrees;
     private Vector mains;
     private String restaurantName;
     private String phone;
     
     public Menu() {
       this.entrees = new Vector();
       this.mains = new Vector();
     }
     
     public Vector getEntrees() { return this.entrees; }
     public void setEntrees(Vector entrees) { this.entrees = entrees; }
     
     public Vector getMains() { return this.mains; }
     public void setMains(Vector mains) { this.mains = mains; }
     
     public String getRestaurantName() { return this.restaurantName; }
     public void setRestaurantName(String restaurantName) {
       this.restaurantName = restaurantName;
     }
     
     public String getPhone() { return this.phone; }
     public void setPhone(String phone) { this.phone = phone; }
     
     public void addEntree(Entree entree) {
       this.entrees.addElement(entree);
     }
     
     public void addMain(Main main) {
       this.mains.addElement(main);
     }
     
 }

以下是 model/Entree.java:

 model/Entree.java
   package model;
public class Entree {
     
     private boolean vegetarian;
     private String name;
     private double price;
     
     public boolean isVegetarian() { return this.vegetarian; }
     public void setVegetarian(boolean vegetarian) {
       this.vegetarian = vegetarian; 
     }
     
     public String getName() { return this.name; }
     public void setName(String name) { this.name = name; }
     
     public double getPrice() { return this.price; }
     public void setPrice(double price) { this.price = price; }
     
   }
   And, finally, here’s model/Main.java:
 package model;
public class Main extends Entree {
     private String description;  
     public String getDescription() { return this.description; }
     public void setDescription(String description) {
       this.description = description; 
     }
}
   

可以看到,因为 Entree 和 Main 共享非常多的特性,因此 Main 类只是扩展了 Entree 类。

创建处理程序

有了这三个模型类之后,现在可以从 XML 文档可靠地创建有效模型了,只要有可以将 XML 转换为 Java 模型对象的处理程序!

要开始创建处理程序,必须扩展 DefaultHandler 类并至少为其三个方法提供实现:

  • startElement(String uri, String localName, String qName, Attributes attributes)
  • endElement(String uri, String localName, String qName)
  • characters(char[] ch, int start, int length)

在 DefaultHandler 类中,这些方法是占位符,替换之后就可以将其注册为 SAXParser 在分析 XML 文档时发送事件通知时的回调事件。此类的 shell 如以下代码清单所示:

 public class MenuHandler extends DefaultHandler {
     public void startElement(
       String uri, String localName, String qName, Attributes attributes) 
       throws SAXException { 
     }
     public void endElement(
       String uri, String localName, String qName) throws SAXException { 
     }
   public void characters(char[] ch, int start, int length) throws SAXException {    
     }  
   }
   The startElement method is called at the start of an element. Thus, the handler needs to be aware of which element is being “started” and what attributes it has, if any. For example, look at the following code listing, which is a snippet from the finished code, or download the full source code:
       // start with the menu root element
       if(qName.equals("menu")) {
         if (menu == null) { 
           String restaurantName = attributes.getValue("restaurantName");
           String phone = attributes.getValue("phone");       
           if(restaurantName == null || phone == null) {
             throw new IllegalArgumentException(
               "A menu must have both restuarantName and phone");
           }
           // if we are here, the menu element is well formed - let's create it
           this.menu = new Menu(); 
           this.menu.setRestaurantName(restaurantName);
           this.menu.setPhone(phone);        
           
         } else {
           throw new IllegalStateException("Cannot have duplicate menu items");
         } 
 

qName 参数提供所遍历的元素的名称。可以利用它来弄清楚元素和相关模型之间的映射。在以上代码中,我将菜单根元素的开始部分映射到 Menu 模型类。属性列表提供了附加到对应元素(在本例中,为 Menu 根元素)的 XML 属性的名称和值。因此,我已经查找到 restaurantName 和 phone 属性的值并将其添加到 Menu 模型。

以上代码还执行某些基本错误检查。例如,如果 startElement 方法遇到另一个 Menu 根元素,它将抛出 IllegalStateException。类似地,如果它找不到 restaurantName 和 phone 属性的有效值,将抛出 IllegalArgumentException。类似地,我们可以创建 Entrée 模型对象。以下代码段显示对应的代码:

     } else if(qName.equals("entree")) {      
         // process the entree element - this must be inside an existing
         // menu element
         if(menu == null) { 
           throw new IllegalStateException("Missing root Menu Element"); 
         } else if(currentEntree != null) {
           throw new IllegalStateException("Already processing an Entree!");        
         } else {
           
           // create the entree and set the vegetarian attribute
           // the rest of the values of the entree will be filled by the 
           // processing of the other elements
           currentEntree = new Entree();
           String vegetarian = attributes.getValue("veg");
          if(vegetarian == null) {
            // assume default value of N
           vegetarian = “N”;
          }
           currentEntree.setVegetarian(vegetarian.equals("Y"));
       }

如代码注释中所示,仅设置了 Entrée 模型的属性。该模型对象的其余内容通过处理其他元素和其他方法(即 endElement 和 characters 方法)来创建。对应的代码段如下所示。

 // from endElement method
   if(qName.equals("entree")) {
   // the entree object is complete
   // add to the menu
   menu.addEntree(currentEntree);
   // we don't need this object now, 
   // set to null
   currentEntree = null; 
   // from characters method
     public void characters(char[] ch, int start, int length)
     throws SAXException {    
       characterValue = 
       new String(ch, start, length).trim();        
   }

characters 方法将其在元素的开始和结束部分间找到的值保存在类变量 (characterValue) 中,方法使用此值来设置模型对象的对应值。以上代码中未显示的是,characters 方法还负责处理任何 CDATA 部分,即将每个 CDATA 部分执行的多个调用追加到此方法之后。

要完整地查看此代码,请参见源代码中 MenuHandler.java 类的完整代码清单。

收尾工作 — 编写分析 MIDlet

由于分析 XML 文档可能是非常耗时的过程,所以应在其单独的线程中执行。XMLParserMidlet 在收到加载文档的指令时会完成这一任务:

     // on user request load the xml file in a separate thread
       if(c == loadCommand) {
         if(runner == null) { runner = new Thread(this); runner.start(); }
         return;
       }

实际分析分为三个步骤:

  • 创建到需要分析的 XML 文档的 InputStream。
  • 加载将 XML 文档转换成对应的模型对象的处理程序。
  • 实例化分析器并开始分析!

MIDlet 内实际运行的方法如下所示:

 public void run() {
       
       // stream to read the file in
       InputStream is = null;
       
       try {
         
         // let's read the XML file and parse it using the SAXParser
         is = getClass().getResourceAsStream("/menu.xml");
      // load the handler
         MenuHandler saxMenuHandler = new MenuHandler();
         
         // instantiate a SAXParser object
         SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
         
         // and parse away
         parser.parse(is, saxMenuHandler);  
         
         // if we are here, there have been no errors, so let us display the 
         // results on the screen
         Menu menu = saxMenuHandler.getMenu();
         
         // first details about the restaurant
         form.append(menu.getRestaurantName());
       form.append(menu.getPhone());
      // then the entrees
         form.append("Entrees\r\n");
         int entreeSize = menu.getEntrees().size();
         for(int i = 0; i < entreeSize; i++) {
           Entree entree = (Entree)menu.getEntrees().elementAt(i);
           form.append(
             entree.getName() + 
             (entree.isVegetarian() ? " (V)" : "") + " - " + entree.getPrice());
           form.append("\r\n");
         }
      // finally, the mains
         form.append("Mains\r\n");
         int mainSize = menu.getMains().size();
         for(int i = 0; i < mainSize; i++) {
           Main main = (Main)menu.getMains().elementAt(i);
           form.append(
             main.getName() + 
             (main.isVegetarian() ? " (V)" : "") + " - " + main.getPrice());
           form.append("\r\n");
           form.append(main.getDescription());
           form.append("\r\n");
         }      
    } catch(Exception e) {
         handleError(e);
       } finally { try { if(is != null) is.close(); } catch(Exception ex) {} }
           
   }

注意代码中执行分析的重要行:

 SAXParser parser = SAXParserFactory.newInstance().newSAXParser();      
   // and parse away
   parser.parse(is, saxMenuHandler);  
   Menu menu = saxMenuHandler.getMenu();

如前面所提到的,XML Parser API 提供实例化 SAXParser 的工厂类。我仅获得它的一个实例并将我的 XML 文件(通过 InputStream)和自定义处理程序传递给它。我在处理程序类中实现了 getMenu() 方法以返回 Menu 模型,该模型用于保存 MIDlet 中的分析结果。

其余代码是用于显示结果的标准 MIDlet 代码。如果没有错误,您将得到如下所示的屏幕:

手机

图 1:最终屏幕

总结

本文介绍了使用 JSR 172 (XML Parser API) 的基础知识。XML Parser API 规定在资源受限的设备中使用 SAX 分析器来分析 XML 文档。在本文中,借助一个工作示例介绍了什么是 SAX 分析器、该 API 是如何定义它的,以及它的最佳使用方式。

另请参见

关于作者

Vikram Goyal 是 Apress 出版的《Pro Java ME MMAPI:Mobile Media API for Java Micro Edition》一书的作者。该书介绍如何向支持 Java 技术的手机添加多媒体功能。Vikram 还是《Jakarta Commons Online Bookshelf》一书的作者,他还帮助管理免费的 craft projects 网站