1. 程式人生 > >jaxb實現XML與JavaBean的互相轉換遇到的難點(一)

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互相轉換 二)待續