JAXP(Java API for XML Processing)为打包器提供了两种不同的处理XML数据的机制,第一种是XML的简单API(Simple API for XML,即SAX),第二种是文档对象模型(Document Object Model,即DOM)。
SAX解析
核心思路:
在SAX模型中,XML文档作为一系列的事件提供给应用程序,每个事件表示XML文档的一种转换。
SAX解析优缺点:
SAX解析的利用事件进行处理可以处理很大的文档,并且不必立即将整个文档读入内存。然而使用XML文档的片段可能会变得复杂,因为开发人员必须跟踪给定片段的所有事件。
SAX是个广泛使用的标准,但不受任何行业团体控制。现在SAX得到开源项目http://www.saxproject.org的支持。
SAX事件模型:
SAX事件包括:文档事件(通知程序一个XML文档的开始和结束)、元素事件(通知程序每个元素的开始和结束)、字符事件(通知程序在元素之间找到的任何字符数据,包括文本、实体和CDATA段),还有不常见的事件:命名空间、实体和实体声明、可忽略的空白、处理指令。
SAX事件处理器:
1. ContentHandler
ContentHanler是任何SAX解析器的核心接口。它定义了在SAX API中最常用的10个回调函数。
2. DefaultHandler
具体实现了ContentHandler接口,允许集中于常用的事件。可以使用自己的子类扩展(extends)DefaultHandler类。
基本的SAX回调函数:
1. 文档回调
public void startDocument() throws SAXException;
SAX通过调用该函数来开始每次解析。
public void endDocument() throws SAXException;
SAX通过调用该函数来表示解析的结束。
2. 元素回调
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException;
qName是元素的名称;元素的属性可以依据名称简单地进行引用。
如,String value = atts.getValue("id"); //返回id属性的值
3. 字符回调
public void characters(char[] ch, int start, int length) throws SAXException;
SAX解析器将字符读入数组ch,start表示偏移,length提供读取的数量。
事件处理器实例:
提供Inventory(公司产品库存)的XML文档,如下:
<?xml version="1.0" encoding="UTF-8"?>
<INVENTORY>
<ITEM>
<SKU>3956</SKU>
<DESCRIPTION>widget</DESCRIPTION>
<QUANTITY>108</QUANTITY>
</ITEM>
<ITEM>
<SKU>5783</SKU>
<DESCRIPTION>gadget</DESCRIPTION>
<QUANTITY>32</QUANTITY>
</ITEM>
<ITEM>
<SKU>6583</SKU>
<DESCRIPTION>rocket</DESCRIPTION>
<QUANTITY>7</QUANTITY>
</ITEM>
</INVENTORY>
事件处理器读取上述XML文档并将其转换为业务对象(Item)。解析之后,客户应用程序才可以进行进一步操作业务对象。代码如下:
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.SAXException;
import org.xml.sax.Attributes;
/**
*从XML文档中解析出库存项目
**/
public class InventoryHandler extends DefaultHandler{
private Item currentItem; //项目模型对象
private ItemCollection items; //解析结果保存器
private StringBuffer characters; //保存元素内容的缓冲区
//字符事件回调函数
public void characters(char[] ch, int start, int length) throws SAXException{
characters.append(ch, start, length);
}
//文档开始事件回调函数
public void startDocument() throws SAXException {
//解析该文档前进行初始化
characters = new StringBuffer();
items = new ItemCollection();
}
//处理每个元素开始的回调函数
public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException {
if(qName.equals("ITEM")){
currentItem = new Item();
}
//为元素内容准备字符缓冲区
characters = new StringBuffer();
}
//处理每个元素结束的回调函数
public void endElement(String uri, String localName, String qName) throws SAXException{
//从缓冲区读取元素内容
String content = charcters.toString();
if(qName.equals("SKU")){
currentItem.setSKU(content);
}
else if(qName.equals("QUANTITY"){
currentItem.setQuantity(content);
}
else if(qName.equals("ITEM"){
items.add(currentItem);
}
}
}
对Inventory.xml的解析过程:(认真理解这个过程)
1. 文档开始,解析器触发startDocumet回调。通过初始化解析器所需的数据结构开始。
2. <INVENTORY>元素,解析器触发它的第一个startElement回调。库存元素没有告诉任何有用的事情,所以可继续前进而无需执行任何特定的处理。
3. <ITEM>元素,解析器触发第二个startElement回调。现在解析有关特定项目的信息。此时可以创建新的数据结构,来保存下一个项目的信息。
4. <SKU>元素,解析器触发另一个startElement回调。将要读取有关SKU的信息,但是必须等待其后SKU自身的character事件。
5. 元素内容,解析器通过一次或多次characters回调来传递SKU数据自身。将字符数据收集到缓冲区中。
6. </SKU>,解析器触发第一个endElement回调,告知已经完成了该元素的读取,并将该内容解释为SKU并将其添加到模型对象中。
7. <QUANTITY>和<DESCRIPTION/>元素。与处理SKU元素的方式大致相同,对每个元素重复步骤4-6。对于这个案例,忽略DESCRIPTION元素。SAX允许保存文档的某些部分,而完全过滤掉其他部分。
8. </ITEM>元素,通过endElement回调,已经完整读取了一个项目。将此项目添加到集合中用来后续检索。
9. </ITEM>,如果这个文档中包含附加的项目,那么将重复步骤3-8,直到读取完所有项目。
10. </INVENTORY>,解析器触发最后一个endElement回调函数。
11. 最后解析器以endDocument回调终止。
创建SAX解析器:
JAXP允许以一种完全可移植的方式来构造解析器,它提供了一种抽象工厂机制,可用于构造解析器,而无需考虑底层实现。
如下:
SAXParser factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
使用SAX解析器解析数据:
输入源:
基于URI的源 >
InputSource fromFile = new InputSource("file://" + fileName);
InputSource fromWeb = new InputSource("http://www.....");
基于流的源 >
InputSource byteSource = new InputSource(someInputStream);
InputSource charSource = new InputSource(someReader);
byteSource.setEncoding("UTF-8"); //设置字节流输入源的编码方案
完整的SAX解析实例:
InventoryHandler类:
import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.SAXException; import org.xml.sax.Attributes; /** *从XML文档中解析出库存项目 **/ public class InventoryHandler extends DefaultHandler { private Item currentItem; // 项目模型对象 private ItemCollection items; // 解析结果保存器 private StringBuffer characters; // 保存元素内容的缓冲区 // 字符事件回调函数 public void characters(char[] ch, int start, int length) throws SAXException { characters.append(ch, start, length); } // 文档开始事件回调函数 public void startDocument() throws SAXException { // 解析该文档前进行初始化 characters = new StringBuffer(); items = new ItemCollection(); } // 处理每个元素开始的回调函数 public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException { if (qName.equals("ITEM")) { currentItem = new Item(); } // 为元素内容准备字符缓冲区 characters = new StringBuffer(); } // 处理每个元素结束的回调函数 public void endElement(String uri, String localName, String qName) throws SAXException { // 从缓冲区读取元素内容 String content = characters.toString(); if (qName.equals("SKU")) { currentItem.setSKU(content); } else if (qName.equals("QUANTITY")) { currentItem.setQuantity(content); } else if (qName.equals("ITEM")) { items.add(currentItem); } }
public ItemCollection getParseResults(){ return items; }
} |
Item类:
public class Item {
private String SKU; private int QUANTITY;
public String getSKU(){ return this.SKU; }
public int getQUANTITY(){ return this.QUANTITY; } public void setQuantity(String content) { // TODO Auto-generated method stub try{ this.QUANTITY = Integer.parseInt(content); } catch(Exception ex){ System.out.println("数字格式异常"); } } public void setSKU(String content) { // TODO Auto-generated method stub this.SKU = content; } } |
ItemCollection类:
import java.util.ArrayList; import java.util.Iterator; public class ItemCollection {
private static ArrayList<Item> itemCollection = new ArrayList<Item>();
public void add(Item currentItem) { itemCollection.add(currentItem); }
public Iterator iterator(){ return itemCollection.iterator(); } } |
以下使用InventoryServlet.java类或者InventoryInfoGet获取解析得到的库存信息
InventoryServlet类
import java.io.IOException; import java.util.Iterator; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.InputSource; import org.xml.sax.SAXException; public class InventoryServlet extends HttpServlet {
//共享解析器工厂的一个实例 private SAXParserFactory parserFactory;
//初始化Servlet public void init() throws ServletException { parserFactory = SAXParserFactory.newInstance(); }
//处理对该Servlet的get请求 public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
//构造自定义的事件处理器 InventoryHandler eventHandler = new InventoryHandler();
//获取SAX解析器实例 SAXParser parser = null; try{ parser = parserFactory.newSAXParser(); } catch(ParserConfigurationException pce){ throw new ServletException("Error constructing parser.", pce); } catch(SAXException se){ throw new ServletException("Error constructing parser.", se); }
try{ //提供XML文档 String uri = "src/Inventory.xml"; InputSource sourceDoc = new InputSource(uri);
//开始解析 parser.parse(sourceDoc, eventHandler); } catch(SAXException se){ throw new ServletException("Error parsign inventory.", se); }
//检查并打印结果 ItemCollection items = eventHandler.getParseResults(); printItems(items, response.getOutputStream());
}
//将结果以HTML打印 private void printItems(ItemCollection items, ServletOutputStream out) throws IOException { Iterator iter = items.iterator();
//建立HTML out.println("<HTML><BODY><H2>Inventory Summary:</H2>"); out.println("<TABLE><TR ALIGN='CENTER'>"); out.println("<TH WIDTH='30%'>SKU</TH>"); out.println("<TH WIDTH='40%'>INVENTORY Quantity</TH></TR>");
//为每一行打印一个表格行 while(iter.hasNext()){ Item currentItem = (Item) iter.next();
out.println("<TR ALIGN='CENTER'>"); out.println("<TD>" + currentItem.getSKU() + "</TD>"); out.println("<TD>" + currentItem.getQUANTITY() + "</TD>"); out.println("</TR>"); }
//关闭HTML out.println("</TABLE></BODY></HTML>"); out.flush();
} } |
InventoryInfoGet类:
import java.io.IOException; import java.util.Iterator; import javax.servlet.ServletException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.InputSource; import org.xml.sax.SAXException; public class InventoryInfoGet {
private SAXParserFactory parserFactory;
//创建SAX解析器工厂 public void createFactory(){ parserFactory = SAXParserFactory.newInstance(); }
//解析方法 public void doParse() throws IOException, ServletException{
//构造自定义的事件处理器 InventoryHandler eventHandler = new InventoryHandler();
//获取SAX解析器实例 SAXParser parser = null; try{ parser = parserFactory.newSAXParser(); } catch(ParserConfigurationException pce){
} catch(SAXException se){
}
try{ //提供XML文档 String uri = "src/Inventory.xml"; InputSource sourceDoc = new InputSource(uri);
//开始解析 parser.parse(sourceDoc, eventHandler); } catch(SAXException se){ throw new ServletException("Error parsign inventory.", se); }
//检查并打印结果 ItemCollection items = eventHandler.getParseResults(); printInfo(items); }
public void printInfo(ItemCollection items){
Iterator iter = items.iterator();
System.out.println("Inventory Summary:"); System.out.println("---------------------"); System.out.println("SKU" + " " + "QUANTITY");
//为每一个Item打印一行 while(iter.hasNext()){ Item currentItem = (Item) iter.next(); System.out.println(currentItem.getSKU() + " " + currentItem.getQUANTITY()); }
}
public static void main(String[]args) throws IOException, ServletException{
InventoryInfoGet iif = new InventoryInfoGet(); iif.createFactory(); //先创建工厂 iif.doParse(); } } |
InventoryInfoGet运行结果如下:
Inventory Summary: --------------------- SKU QUANTITY 3956 108 5783 32 6583 7 |
到此为止,我们已经对SAX解析的原理及其基本实现有一个大概的了解了。
另外上面必须注意:
SAX解析器不是线程安全的,虽然可以重复使用一个解析器来解析几个文档,但是不能重复使用它同时解析几个文档。因此,Servlet的get方法为每个用户构造一个解析器,以允许Servelt同时处理多个用户。
不能在线程之间共享解析器实例,但是可以在特定环境下共享SAXParserFactory。Servelet仅需要维护工厂的一个实例,并且它反复地在Servlet的get方法内引用该实例。这在多线程环境中是有效的,因为SAXParserFactory的newSAXParser()方法声明为线程安全的。所以可以在很多线程中安全地使用工厂对象。
SAX进阶知识:
配置解析器:
激活DTD文档确认 >
如果XML文档引用一个DTD,那么应该激活DTD确认。这样当SAX解析器解析时,它将依据DTD来检查文档结构;如果文档结构与DTD不匹配,那么解析器将以错误响应。
代码如下:
激活命名空间 >
SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setValidating(true); SAXParser validatingParser = factory.newSAXParser(); |
处理高级事件:
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException;
使用该回调来捕获不必要的空白。如果希望在解析期间保持不重要的空白,就需要该回调。
SAXParserFactory factory = SAXParserFactory.newInstance(); factory.setNamespaceAware(true); SAXParser validatingParser = factory.newSAXParser(); |
如果文档包含任何XML处理指令,就必须实现该回调
target是处理指令的名称,data参数包括处理指令名称后的任何内容。
如果显示地告诉解析器不用解析引用,它就可能跳过一些实体。
ErrorHandler类:
实现SAXErrorHandler接口以捕获不同类型的XML解析错误。
ErrorHandler为3种不同类型的解析错误定义了回调:
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException; |
Fatal Error,致命错误,最严重,包括语法错误。
Error,比如与DTD不匹配。
Warnings,最轻度严重性。
后记:
当性能非常重要时,使用SAX解析,也可以使用SAX解析大型文档。基于SAX的解析一般比基于DOM的解析更快且占用更少内存。