手把手教你解析XML

期望通过每一次分享,让技术的门槛变低,落地更容易。 —— 周围

本文适用范围

  • 实际工作中遇到关于解析和转换xml的问题
  • 不懂,想详细了解学习如何解析xml的同学

本文目的

通过本文,你将解决如下问题:

  • xml数据结构对应的java实体结构是如何绑定的
  • 如何将xml结构数据转换成对象
  • 如何将对象转换为xml

原始数据

<RESPONSE>
    <ROW>
        <id>E37AA69997D0F399E040007F0100484D</id>
        <guid>20130809103931E37AA70270E78822E040007F01004851@gd.zg</guid>
        <qryid>FR20130809103441000000001054@90001113@gd.zg</qryid>
        <todepturi>40001009@jm.gd.cp</todepturi>
        <todeptname>中共江门市委办秘书科</todeptname>
        <sendamount>1</sendamount>
        <status>RC01</status>
        <sendtime>2013-08-09 10:39:32</sendtime>
        <recvtime>2013-08-09 10:35:21</recvtime>
        <appuri>201308071@jm.gd.cp</appuri>
        <appname>江门市委办法规文件报备系统</appname>
        <senddocstatus>
            <status exchid="E201308090300709@11000003@gd.zg">
                <site type="NT01" uri="11000003@gd.zg" name="广东省委机要局交换箱" status="PS03" stadesc="已经处理" modify="2013-08-09 10:39:34" errcode="0" final="0"/>
                <site type="NT03" uri="201308071@jm.gd.cp" name="江门市委办法规文件报备系统" status="PS03" stadesc="已经处理" modify="2013-08-09 10:39:45" errcode="0" final="0" wsaddr="http://172.17.4.241:8080/fgsys/services/datasynservice"/>
            </status>
        </senddocstatus>
        <printedinfo>
            <rcpt>
                <exchid>E201310290563707@20000007@jm.gd.cp-print</exchid>
                <prints>
                    <operuri>21800168@jm.gd.cp</operuri>
                    <opername>测试用户2</opername>
                    <operdate>2013-10-30 9:19:55</operdate>
                    <print num="0000016"/> <!--公文顺序号 没有流水号print节点为空-->
                    <print num="0000017"/>
                </prints>
            </rcpt>
        </printedinfo>
    </ROW>
</RESPONSE>

上述的xml格式基本涵盖了标签值、标签属性值、集合等内容,我们了解了针对特殊数据格式的处理,解析起来就没问题了,下面所讲的xml例子全部来源于上面的xml原始数据。

前置环境

你需要导入xstream.jar的包

需要注意的细节

  • xml标签区分大小写,映射的时候不要认为它能够像sql一样,不管大小写
  • xml标签对应实体属性,应确保数据类型是一致的,不然拿不到数据或者报错

解释格式

下面将对常见的xml和后端java-model做一个对应格式映射

属性格式

<id>E37AA69997D0F399E040007F0100484D</id>
<guid>20130809103931E37AA70270E78822E040007F01004851@gd.zg</guid>

上述这种xml标签是节点标签名称如 <id>,并且内容数据是String类型, 对应到java实体就是下面的声明:

/** E37AA69997D0F399E040007F0100484D */
private String id;
/** 20130809103931E37AA70270E78822E040007F01004851@gd.zg */
private String guid;

对象格式

<prints>
    <operuri>21800168@jm.gd.cp</operuri>
    <opername>测试用户2</opername>
    <operdate>2013-10-30 9:19:55</operdate>
</prints>

如上述的格式,它对应的就是<prints>标签是一个对象,它下面包含了3个子节点<operuri><opername><operdate>。我们要将它们转换应该就是对象封装了,如下:

@XStreamAlias("prints")//标签名称是小写的prints,但是java对象声明是首字母大写不一致
@XStreamConverter(value=com.thoughtworks.xstream.converters.extended.JavaClassConverter.class)
public class Prints {

	/** 21800168@jm.gd.cp */
	private String operuri;

	/** 测试用户2 */
	private String opername;

	/** 2013-10-30 9:19:55 */
	//此处的注解也是别名替换,声明绑定xml标签时,使用operdate,而不是属性名operdate1
	@XStreamAlias("operdate")
	private String operdate1;
	
	... (省略后续字段 及相关get/set方法)
}

接着,可以自己在对象里构建一个main方法做测试:

public static void main(String[] args) {
	String xml= "<prints><operuri>21800168@jm.gd.cp</operuri><opername>测试用户2</opername><operdate>2013-10-30 9:19:55</operdate></prints>";
	//com.leadal.csa.common.utils.XmlUtil
	Prints prints = (Prints) XmlUtil.Xml2Object(xml, Prints.class);
	System.out.println(prints);	
}

好了,完成了第一个转换测试,接下来要继续介绍另一种对象格式的解析。

<site type="NT03" uri="201308071@jm.gd.cp" name="江门市委办法规文件报备系统" status="PS03" stadesc="已经处理" modify="2013-08-09 10:39:45" errcode="0" final="0" wsaddr="http://172.17.4.241:8080/fgsys/services/datasynservice"/>

按照之前的理解,创建这个标签应该叫<site>,那我们构造对应对象就可以了,但是你会发现,它没有子节点,只有像html标签一样的标签属性值,同样也可以提取,不过要换一个方式,下面提供2种方法:

@XStreamAlias("site")
@XStreamConverter(value=com.thoughtworks.xstream.converters.extended.JavaClassConverter.class)
public class Site {

	/** NT01 */
	@XStreamAsAttribute
	private String type;
	
	/** 11000003@gd.zg */
	@XStreamAsAttribute
	private String uri;
	
	... (省略后续字段 及相关get/set方法)

上述第一种方式就是在属性值上添加@XStreamAsAttribute注解,可以实现提取<site type="NT03">的type属性。

@XStreamAlias("site")
@XStreamConverter(value=com.thoughtworks.xstream.converters.extended.ToAttributedValueConverter.class)
public class Site {

	/** NT01 */
	private String type;
	
	/** 11000003@gd.zg */
	private String uri;
	
	... (省略后续字段 及相关get/set方法)

而第二种方式,属于全局设置,更改类上面@XStreamConverter注解中的转换器ToAttributedValueConverter.class,可完成提取<site>的所有标签属性,直接绑定到对应的参数名称上。

集合格式

<status exchid="E201308090300709@11000003@gd.zg">
    <site type="NT01" uri="11000003@gd.zg" name="广东省委机要局交换箱" status="PS03" stadesc="已经处理" modify="2013-08-09 10:39:34" errcode="0" final="0"/>
    <site type="NT03" uri="201308071@jm.gd.cp" name="江门市委办法规文件报备系统" status="PS03" stadesc="已经处理" modify="2013-08-09 10:39:45" errcode="0" final="0" wsaddr="http://172.17.4.241:8080/fgsys/services/datasynservice"/>
</status>

如上述所见的格式,<status>中包含了2个<site>,很明显<status>里面声明的关于site内容应该是集合类型。

@XStreamAlias("status")
@XStreamConverter(value=com.thoughtworks.xstream.converters.extended.JavaClassConverter.class)
public class SendDocStatus {

	/** E201308090300709@11000003@gd.zg */
	@XStreamAsAttribute
	private String exchid;
	
	/** 状态记录 */
	@XStreamImplicit //修饰site对应的标签是集合,会出现多个
	private List<Site> site;
	
	...(省略后续字段 及相关get/set方法)

如上述代码,@XStreamImplicit完成了对集合类型的修饰,如果不声明该注解,可以试试解析会报什么错,应该会提醒你标签重复了,因为程序不知道它是集合类型,无法接收到。

具体操作

首先我们得有一个概念:在java里,一切都是对象,xml数据也不例外。
那么,我们要针对xml标签名称去创建对应的实体对象

如下图,先创建一个实体对象,将xml的对象对应到了该对象上,要求是数据格式一致。(这里只抽取了 <ROW> 对象展示)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yXuVPQH9-1637921539708)(file:///C:/Users/Dell/Desktop/工作文档/blog/xml/xml1.png)]

结合前面的格式讲解,现在再根据这个图的指向,应该能清楚xml和java实体是如何对应的了。

关于如何调用API在java实体与xml数据之间转换,看下面的例子,使用的均是
com.leadal.csa.common.utils.XmlUtil

String xml= "<prints><operuri>21800168@jm.gd.cp</operuri><opername>测试用户2</opername><operdate>2013-10-30 9:19:55</operdate></prints>";
//xml数据 转 java实体
Prints prints = (Prints) XmlUtil.Xml2Object(xml, Prints.class);
//java实体 转 xml数据
XmlUtil.Object2Xml(prints, prints.getClass());

常见异常错误

No such field xxx

com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter$UnknownFieldException: 
No such field com.leadal.csa.common.model.SendDocStatus.status

该错误是找不到对应的字段,如上述的错误案例,是找不到SendDocStatus.status下面的这个status属性。
通常这种错误,去检查字段属性名称是否拼错了,或者是否加了别名属性声明,导致最后无法与标签名称绑定上。

Duplicate field xxx

Exception in thread "main"
com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter$DuplicateFieldException: 
Duplicate field print

这个错误,翻译过来是重复字段print,意思就是print这个属性重复出现了,但你的接收类型装不下多个,这种情况就要看自己是不是要根据xml结构,去把这个print变成集合。
我们看一下原始数据关于print的描述:

...
<prints>
    ...
    <print num="0000016"/> 
    <print num="0000017"/>
</prints>
...

出现了2次这个标签,并且是以标签属性的方式设置值的,那么怎么设置类型才能将其成功封装到外部的<prints>标签中呢?如下:

@XStreamAlias("prints")
@XStreamConverter(value=com.thoughtworks.xstream.converters.extended.JavaClassConverter.class)
public class Prints {

	@XStreamImplicit//此注解就是集合注解,参数名正好与<print>标签名一致
	private List<Print> print;
	
	....(省略后续字段 及相关get/set方法)
}

@XStreamAlias("print")
@XStreamConverter(value=com.thoughtworks.xstream.converters.extended.ToAttributedValueConverter.class)
public class Print {

	/**
	 * <print num="0000016"/> 
	 * 这里的num正好对应标签属性值名称
	 */
	private String num;

	....(省略后续字段 及相关get/set方法)
}

写在最后

以上就是关于如何解析xml的方式方法和将对象转成xml的方式,有问题可以再详细沟通,会使用到的注解我在下面统一列出并加上注释。

本文注解

//声明当前对象或属性名称的别名,必须与xml标签名一致,区分大小写
@XStreamAlias("prints")
//声明xml解析转换器,
//JavaClassConverter是普通java对象转换器
//ToAttributedValueConverter是适用于标签内属性值较多的转换器
@XStreamConverter(value=com.thoughtworks.xstream.converters.extended.JavaClassConverter.class)
//集合类型声明
@XStreamImplicit