您的位置:首页 > 编程语言 > Java开发

jaxb实现XML与JavaBean的互相转换遇到的难点(一)

2017-11-11 16:02 579 查看
1.首先交代一下背景:

入职后项目组交给我的第一个任务便是做一个酒店直连对接,流程说白了就是发xml报文去携程的系统,携程返回xml里面包含了酒店相关信息,流程听着很简单,但是涉及到跟携程对接、跟公司内部系统对接,作为一个中间层,项目进度很难自己把控,再加上酒店这块业务也挺复杂的,看相关文档就花了两天梳理,所以也很是头疼。

下面说一下技术遇到的难点:事前拿postman往携程系统请求一个酒店的静态信息,结果返回了4M多的xml数据,postman直接崩掉;所以用什么方式解析XML报文要仔细考。Java经常用dom4j解析xml,但是dom4j会将整个XML文档加载到内存中,返回的报文这么大将消耗大量内存;SAX是基于流解析xml的一种技术,但是SAX是读取一段解析一段xml的,速度自然而然非常慢。

上面两种常见的xml解析技术很快被我否定,接着我很快地想到jaxb,jdk6之后它就成为了官网的工具,而且采用的是对象属性与xml的映射;问了一下携程对接人员,没有.xsd文件,可能手动生成那么多对象会比较累,当时觉得麻烦可能仅限于此。

2.实际开工遇到的问题

放上XML就很明显易见了

<Request>
<Header  AllianceID="12345" SID="12345" TimeStamp="1509679923"  RequestType="OTA" Signature="12345"/>
<HotelRequest>
<RequestBody xmlns:ns="http://www.lyyco.cc/OTA/2003/05" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<OTA_HotelDescriptiveInfoRQ Version="1.0" xsi:schemaLocation="http://www.lyyco.cc/OTA/2003/05 OTA.xsd" xmlns="http://www.lyyco.cc/OTA/2003/05" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4000
;
<HotelDescriptiveInfos>
<HotelDescriptiveInfo HotelCode="625">
<HotelInfo SendData="true"/>
<FacilityInfo SendGuestRooms="true"/>
<AreaInfo SendAttractions="true"SendRecreations="true"/>
<ContactInfo SendData="true"/>
<MultimediaObjects SendData="true"/>
</HotelDescriptiveInfo>
</HotelDescriptiveInfos>
</OTA_HotelDescriptiveInfoRQ>
</RequestBody>
</HotelRequest>
</Request>


请求的xml报文中有好几处命名空间,如果不做对应处理我用Java对象生成的xml是不会有xmlns、xmlns:ns 等命名空间的,这样发送的请求不能进入携程的系统,请求失败。

一般情况下,所有的xml都是有.xsd文件进行格式约束的,况且本来也不建议在非根节点处定义命名空间,于是花了挺长时间研究了如何使生成的xml报文里面有对应的Namespace。先上几个测试类

import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.*;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import com.banma.ota.utils.JaxbUtils;
import com.banma.ota.utils.XmlUtil;

@XmlRootElement(name="ClassA")
@XmlAccessorType(XmlAccessType.FIELD)
public class ClassA {
@XmlElement(name="ClassAID")
private int classAId;
@XmlElement(name = "ClassAName")
private String ClassAName;
@XmlElement(namespace="http://www.cnblogs.com")
private ClassB ClassB;
@XmlElementWrapper
@XmlElement(name = "ClassC")
private List<ClassC> classCs;

public String getClassAName() {
return ClassAName;
}
public void setClassAName(String classAName) {
ClassAName = classAName;
}
public ClassB getClassB() {
return ClassB;
}
public void setClassB(ClassB classB) {
ClassB = classB;
}
public int getClassAId() {
return classAId;
}
public void setClassAId(int classAId) {
this.classAId = classAId;
}
public List<ClassC> getClassCs() {
return classCs;
}
public void setClassCs(List<ClassC> classCs) {
this.classCs = classCs;
}
}


import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
public class ClassB {

@XmlAttribute
private int ClassBId;
@XmlAttribute
private String ClassBName;

public int getClassBId() {
return ClassBId;
}

public void setClassBId(int classBId) {
this.ClassBId = classBId;
}

public String getClassBName() {
return ClassBName;
}

public void setClassBName(String classBName) {
this.ClassBName = classBName;
}

}


import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
@XmlAccessorType(XmlAccessType.FIELD)
public class ClassC {

@XmlElement(name="Test")
private String test;

public String getTest() {
return test;
}

public void setTest(String test) {
this.test = test;
}

}


我理想生成的xml是这个样子的:

<?xml version="1.0" encoding="UTF-8"?>
<ClassA xmlns="http://www.cnblogs.com">
<ClassAID>1</ClassAID>
<ClassAName>1</ClassAName>
<ClassB xmlns="http://www.cnblogs.com" classBId="22" classBName="B2"></ClassB>
<classCs>
<ClassC>
<Test>lyy</Test>
</ClassC>
<ClassC>
<Test>tomorrow</Test>
</ClassC>
</classCs>
</ClassA>


但是假如不对Jaxb做对应处理,直接映射得到的xml是这样的:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ClassA xmlns:ns2="http://www.cnblogs.com">
<ClassAID>1</ClassAID>
<ClassAName>1</ClassAName>
<ns2:ClassB classBId="22" classBName="B2"/>
<classCs>
<ClassC>
<Test>lyy</Test>
</ClassC>
<ClassC>
<Test>tomorrow</Test>
</ClassC>
</classCs>
</ClassA>


存在一个命名空间前缀的问题,以及ClassB节点与预期不相符,直接变成ns2前缀了。

下面放出我的解决办法

import java.io.StringReader;
import java.io.StringWriter;
import javax.xml.bind.*;
import javax.xml.transform.sax.SAXSource;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLFilterImpl;
import org.xml.sax.helpers.XMLReaderFactory;

/**
* 带有多个命名空间的XML与javabean转换工具类
*/
public class XmlUtil {

public static String toXML(Object obj) {
try {
JAXBContext context = JAXBContext.newInstance(obj.getClass());

Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");// //编码格式
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);// 是否格式化生成的xml串
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, false);// 是否省略xm头声明信息

StringWriter out = new StringWriter();
OutputFormat format = new OutputFormat();
format.setIndent(true);
format.setNewlines(true);
format.setNewLineAfterDeclaration(false);
XMLWriter writer = new XMLWriter(out, format);
/*
* 通过XMLFiltereImpl匿名内部类实现命名空间和XML节点名称的控制
*/
XMLFilterImpl nsfFilter = new XMLFilterImpl() {
private boolean ignoreNamespace = false;
private String rootNamespace = null;
private boolean isRootElement = true;

@Override
public void startDocument() throws SAXException {
super.startDocument();
}

@Override
public void startElement(String uri, String localName, String qName, Attributes atts)
throws SAXException {
if (this.ignoreNames
a6b2
pace) {
uri = "";
}
if (this.isRootElement) {
this.isRootElement = false;
} else if (!"".equals(uri) && !localName.contains("xmlns")) {
localName = localName + " xmlns=\"" + uri + "\"";
}
super.startElement(uri, localName, localName, atts);
}

@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (this.ignoreNamespace){
uri = "";}
super.endElement(uri, localName, localName);
}

@Override
public void startPrefixMapping(String prefix, String url) throws SAXException {
if (this.rootNamespace != null){
url = this.rootNamespace;}
if (!this.ignoreNamespace){
super.startPrefixMapping("", url);}
}
};
nsfFilter.setContentHandler(writer);
marshaller.marshal(obj, nsfFilter);
return out.toString();

} catch (Exception e) {
throw new RuntimeException(e);
}
}


核心思路是通过XMLFilterImpl来控制生成的xml节点名称,这样输出的xml与预期的百分百符合;

解决了第一个坑,第二个坑则是解析返回的xml报文时遇到的,同样跟命名空间有关系。

(jaxb实现xml与javabean互相转换 二)待续
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: