Java解析xml的三种方式

  在Java世界中,xml是一种重要的数据格式,很多开源框架包括Spring、MyBatis等都使用了xml文档作为配置文件,了解如何解析xml文档是非常有必要的。

  常见使用JDK本身自带的API能够解析xml文件的方式一共有三种,分别是DOM(Document Object Model)、SAX(Simple API for XML)、StAX(Streaming API for API)。

  在下面的示例中,都使用下示的demo.xml作为解析的样例,其内容如下:

<?xml version="1.0" encoding="UTF-8"?>  <!-- 文档头,定义编码格式和xml标准版本 -->
<!DOCTYPE demo>    <!-- 文档类型定义,demo表示这个xml文档的根节点标签的元素名,
                       一般还会在这里定义dtd文件,可以用来检验文档格式 -->
<demo>   <!-- xml严格要求有开标签必须有闭标签 -->
    <mobile country="China">
        <company>HUAWEI</company>
        <model>meta 20 Pro</model>
        <price>5699</price>
        <year>2018</year>
    </mobile>
    <mobile country="China">
        <company>XIAOMI</company>
        <model>小米max2</model>
        <price>2899</price>
        <year>2017</year>
        <country>China</country>
    </mobile>
    <mobile country="USA">
        <company>APPLE</company>
        <model>iphone 7 plus</model>
        <price>5799</price>
        <year>2016</year>
        <country>USA</country>
    </mobile>
</demo>


一、DOM

  DOM是文档对象模型的意思,使用这种方式解析xml文档,会将整个文件加载到内存中并构建一个DOM树,基于这颗树形结构对各个节点(Node)进行操作。

  XML文档中的每个成分都是一个节点:整个文档是一个文档节点,每个标签对应一个元素节点,包含在标签中的文本是文本节点,每一个XML树形是一个属性节点,注释属于注释节点。

  先读取文档,获得Document对象,代码如下所示:

    // 根据XML文件路径获得Document对象
    public static Document getXmlDocument(String xmlPath) throws Exception {
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        
        documentBuilderFactory.setValidating(true);                         // 是否指定此代码生成的解析器将在文档解析时验证文档
        documentBuilderFactory.setNamespaceAware(false);                    // 是否指定此代码生成的解析器将为XML命名空间提供支持
        documentBuilderFactory.setIgnoringComments(true);                   // 是否指定此代码生成的解析器将忽略注释
        documentBuilderFactory.setIgnoringElementContentWhitespace(false);  // 是否指定此工厂创建的解析器必须在解析XML文档时消除元素内容中的空格(有时称为“可忽略的空白”)
        documentBuilderFactory.setCoalescing(false);                        // 是否指定此代码生成的解析器将CDATA节点转换为文本节点并将其附加到相邻(如果有的话)文本节点
        documentBuilderFactory.setExpandEntityReferences(true);             // 是否指定此代码生成的解析器将扩展实体引用节点
        
        // 创建DocumentBuilder
        DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();      
        // 设置异常处理对象
        builder.setErrorHandler(new ErrorHandler() {
            
            @Override
            public void warning(SAXParseException exception) throws SAXException {
                // TODO Auto-generated method stub
            }
            
            @Override
            public void fatalError(SAXParseException exception) throws SAXException {
                // TODO Auto-generated method stub
            }
            
            @Override
            public void error(SAXParseException exception) throws SAXException {
                // TODO Auto-generated method stub
            }
        });
        
        return builder.parse(xmlPath);
    }

  将其放在一个工具类XMLParseUtils中,方便调用,下面使用XPath配合DOM解析xml。

public class DOMTest {
    public static void main(String[] args) throws Exception {
        Document doc = XMLParseUtils.getXmlDocument("src/main/java/demo.xml");
        
        // 创建 XPathFactory
        XPathFactory factory = XPathFactory.newInstance();
        // 创建 XPath对象
        XPath xpath = factory.newXPath();       
        // 编译 XPath表达式
        // 表达式字符串定义了获取节点的规则
        // 下面这条表达式获取model子节点值为meta 20 Pro的mobile节点下的price节点的值
        XPathExpression expr = xpath.compile("//mobile[model='meta 20 Pro']/price/text()");

        // 通过XPath表达式得到结果,第一个参数指定了XPath表达式进行查询的上下文节点,也就是在指定节点下查找符合XPath的节点
        // 本例中的上下文节点时整个文档;第二个参数指定了XPath表达式的返回类型
        Object result = expr.evaluate(doc, XPathConstants.NODESET);
        System.out.println("查询型号为meta 20 Pro的手机的价格:");
        NodeList nodes = (NodeList) result;   // 强制类型转换,至于转换后的类型要看XPathExpression.evaluate方法returnType的设置
        for (int i = 0; i < nodes.getLength(); i++) {
            System.out.println(nodes.item(i).getNodeValue());           
        }   
    }
}
# console:
查询型号为meta 20 Pro的手机的价格:
5699

  现在想获取所有中国公司出品的手机详细信息,代码如下:

System.out.println("查询所有中国手机型号:");
NodeList nodes2 = (NodeList) xpath.evaluate("//mobile[@country='China']/model/text()", doc, XPathConstants.NODESET);
for (int i = 0; i < nodes2.getLength(); i++) {
    System.out.println(nodes2.item(i).getNodeValue());
}   
# console:
查询所有中国手机型号:
meta 20 Pro
小米max2


二、SAX

  SAX是一种使用事件回调机制的XML解析器,事件由解析器产生并通过回调函数发送给应用程序,这种模式称为“推模式”

SAX推模式.png

  跟上面的类似,首先会创建一个SAX解析器工厂对象(SAXParserFactory),工厂对象创建解析器对象(SAXParser),解析器解析文档流对象的调用方法形式为:

  parse(InputStream, DefaultHandler);
# (1)InputStream是读取文档得到的流对象
# (2)DefaultHandler是一个实现了DefaultHandler接口的对象解析流对 
#     象时解析流对象时会触发回调的方法都在该接口中定义,所以实现了
#     接口的类必须实现想要进行处理的事件对应的回调方法

  如下所示,定义了一个DefaultHandler接口的实现类,实现了在解析文档流对象的开始和结束时触发的方法,在解析每个标签元素节点开始和结束时触发的方法,在解析文本节点时触发的方法:

    static class TestSAXHandler extends DefaultHandler {
        
        // SAX开始解析文档时会调用本方法
        @Override
        public void startDocument () throws SAXException {
            System.out.println("parse xml document start!");
        }
        
        // SAX解析文档结束时会回调本方法
        @Override
        public void endDocument () throws SAXException {
            System.out.println("parse xml document end!");
        }

        // SAX解析每个标签元素时会回调本方法
        @Override
        public void startElement (String uri, String localName,
                String qName, Attributes attributes) throws SAXException {
            System.out.println("开始解析标签:" + qName);
            int length = attributes.getLength();
            for (int i = 0; i < length; i++) {
                String aname = attributes.getLocalName(i);
                String avalue = attributes.getValue(i);
                System.out.println("属性名:" + aname);
                System.out.println("属性值:" + avalue);
            }
        }
        
        // SAX解析每个标签结束时都会回调本方法
        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            System.out.println("解析标签结束:" + qName);
        }
        
        // SAX解析每一块文本内容时都会回调本方法,空格、换行、开标签的标签头也会被视为文本
        public void characters (char ch[], int start, int length) throws SAXException {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i <= length; i++) {
                sb.append(ch[start + i]);
            }
            System.out.println("\"" + sb.toString() + "\"");
        }
    }

  将文档转换为流对象,在XMLParseUtils中新增一个静态方法,代码如下:

    public static InputStream getXmlInputStream(String xmlPath) {
        InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(xmlPath);
        return inputStream;
    }

  在SAXTest的main方法中解析xml的代码如下:

public class SAXTest {
    public static void main(String[] args) throws Exception {
        InputStream inputStream = XMLParseUtils.getXmlInputStream("demo.xml");
        
        SAXParserFactory factory = SAXParserFactory.newInstance();      
        SAXParser parser = factory.newSAXParser();
        parser.parse(inputStream, new TestSAXHandler());
    }
# console:
parse xml document start!
开始解析标签:demo
"
    <"
开始解析标签:mobile
属性名:country
属性值:China
"
        <"
开始解析标签:company
"HUAWEI<"
解析标签结束:company
"
        <"
开始解析标签:model
"meta 20 Pro<"
解析标签结束:model
"
        <"
开始解析标签:price
"5699<"
解析标签结束:price
"
        <"
开始解析标签:year
"2018<"
解析标签结束:year
"
    <"
解析标签结束:mobile
"
    <"
开始解析标签:mobile
属性名:country
属性值:China
"
        <"
开始解析标签:company
"XIAOMI<"
解析标签结束:company
"
        <"
开始解析标签:model
"小米max2<"
解析标签结束:model
"
        <"
开始解析标签:price
"2899<"
解析标签结束:price
"
        <"
开始解析标签:year
"2017<"
解析标签结束:year
"
        <"
开始解析标签:country
"China<"
解析标签结束:country
"
    <"
解析标签结束:mobile
"
    <"
开始解析标签:mobile
属性名:country
属性值:USA
"
        <"
开始解析标签:company
"APPLE<"
解析标签结束:company
"
        <"
开始解析标签:model
"iphone 7 plus<"
解析标签结束:model
"
        <"
开始解析标签:price
"5799<"
解析标签结束:price
"
        <"
开始解析标签:year
"2016<"
解析标签结束:year
"
        <"
开始解析标签:country
"USA<"
解析标签结束:country
"
    <"
解析标签结束:mobile
"
<"
解析标签结束:demo
parse xml document end!


三、StAX

  StAX与SAX类似,也是基于文档流对象和产生事件解析XML的模式,不过事件不同与SAX的回调通知方式,需要应用程序自行遍历判断事件类型,从中筛选出要获取的节点的信息,所有事件类型都在XMLStreamConstants中定义,常见的有:

事件 字典
解析开标签节点事件 XMLStreamConstants.START_ELEMENT
闭标签节点事件 XMLStreamConstants.END_ELEMENT
文本节点事件 XMLStreamConstants.CHARACTERS

  StAX这种解析的策略也被成为“拉模式”

StAX拉模式.png



  同样的,StAX首先要获取XML文档流对象,然后创建解析器工厂对象(XMLInputFactory),根据工厂对象创建解析器对象(XMLStreamReader),这里的解析器实际上就是一个迭代器,根据迭代器可以顺序获取事件类型,并根据事件类型去调用解析器的其他方法获取节点内容进行处理,使用样例如下所示:

public class StAXTest {
    public static void main(String[] args) throws Exception {
        InputStream in = XMLParseUtils.getXmlInputStream("inventory.xml");      
        XMLInputFactory factory = XMLInputFactory.newFactory();
        XMLStreamReader parser = factory.createXMLStreamReader(in);
        while (parser.hasNext()) {
            int event = parser.next();
            // 解析开标签
            if (event == XMLStreamConstants.START_ELEMENT) {    
                System.out.println("解析标签元素: " + parser.getLocalName());
                int attCount = parser.getAttributeCount();
                System.out.println("该标签属性数量: " + attCount);
                for (int i = 0; i < attCount; i++) {
                    System.out.println("属性名:" + parser.getAttributeLocalName(0));
                    System.out.println("属性值:" + parser.getAttributeValue(0));
                }
                System.out.println();
                continue;
            }
            // 解析文本
            if (event == XMLStreamConstants.CHARACTERS) {
                System.out.println("文本内容: \"" + parser.getText() + "\"");
                System.out.println();
                continue;
            }
            // 解析闭标签
            if (event == XMLStreamConstants.END_ELEMENT) {
                System.out.println("解析标签元素结束: " + parser.getLocalName());
                System.out.println();
            }
        }
    }
}
# console:
解析标签元素: demo
该标签属性数量: 0

文本内容: "
    "

解析标签元素: mobile
该标签属性数量: 1
属性名:country
属性值:China

文本内容: "
        "

解析标签元素: company
该标签属性数量: 0

文本内容: "HUAWEI"

解析标签元素结束: company

文本内容: "
        "

解析标签元素: model
该标签属性数量: 0

文本内容: "meta 20 Pro"

解析标签元素结束: model

文本内容: "
        "

解析标签元素: price
该标签属性数量: 0

文本内容: "5699"

解析标签元素结束: price

文本内容: "
        "

解析标签元素: year
该标签属性数量: 0

文本内容: "2018"

解析标签元素结束: year

文本内容: "
    "

解析标签元素结束: mobile

文本内容: "
    "

解析标签元素: mobile
该标签属性数量: 1
属性名:country
属性值:China

文本内容: "
        "

解析标签元素: company
该标签属性数量: 0

文本内容: "XIAOMI"

解析标签元素结束: company

文本内容: "
        "

解析标签元素: model
该标签属性数量: 0

文本内容: "小米max2"

解析标签元素结束: model

文本内容: "
        "

解析标签元素: price
该标签属性数量: 0

文本内容: "2899"

解析标签元素结束: price

文本内容: "
        "

解析标签元素: year
该标签属性数量: 0

文本内容: "2017"

解析标签元素结束: year

文本内容: "
        "

解析标签元素: country
该标签属性数量: 0

文本内容: "China"

解析标签元素结束: country

文本内容: "
    "

解析标签元素结束: mobile

文本内容: "
    "

解析标签元素: mobile
该标签属性数量: 1
属性名:country
属性值:USA

文本内容: "
        "

解析标签元素: company
该标签属性数量: 0

文本内容: "APPLE"

解析标签元素结束: company

文本内容: "
        "

解析标签元素: model
该标签属性数量: 0

文本内容: "iphone 7 plus"

解析标签元素结束: model

文本内容: "
        "

解析标签元素: price
该标签属性数量: 0

文本内容: "5799"

解析标签元素结束: price

文本内容: "
        "

解析标签元素: year
该标签属性数量: 0

文本内容: "2016"

解析标签元素结束: year

文本内容: "
        "

解析标签元素: country
该标签属性数量: 0

文本内容: "USA"

解析标签元素结束: country

文本内容: "
    "

解析标签元素结束: mobile

文本内容: "
"

解析标签元素结束: demo



四、三种方式的比较

  DOM 的优点在于面向节点树编程比较简单,也比较好理解,在解析DOM时就已经完整加载了文档树,对节点的遍历和导航(包括父节点、子节点、兄弟节点)比较方便,也易于添加和删除节点,但是在文档内容比较大的时候,性能消耗比较大,处理效率较低,不过一般都用作配置文件,内容不多,因此忽略不计。

  如果xml文件本身内容较多,而且在很多情况下只想解析某一个节点而不想加载全部节点浪费资源,这时可考虑使用流机制的解析器SAX和StAX,能够降低性能消耗,提高效率。

  SAX 的缺点非常明显,没有加载完整的文档结构,对节点信息的获取和处理依赖回调函数,当处理逻辑涉及多个多层节点之间的关系时,回调函数的逻辑会非常复杂和难以维护;而且流处理方式只允许从上往下处理,不允许回溯已经处理过的节点,另外SAX也不支持修改XML。

  StAX 具有跟SAX一样的流处理的缺点,StAX包括了两套处理XML文档的API:一种是基于指针的API,效率高但是抽象化程度低;另一种是基于事件迭代器的API,效率低但是抽象化程序高。开发者可以根据需求做平衡和选择。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容