jaxb實現XML與JavaBean的互相轉換遇到的難點(一)
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">
<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.ignoreNamespace) {
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互相轉換 二)待續