控制JAXB的輸入輸出

上一節介紹瞭如何在解析模型的時候構建模型之間的父子鏈,其實使用afterUnmarshal()或beforeUnmarshal()方法或Unmarshaller.Listener都可以用來參與到模型的解析過程,也就是輸入過程。關於輸入過程的參與沒有過多的說明,這節主要介紹輸出的參與。

 

一般情況下,所有聲明的jaxb的屬性和元素都會事無鉅細的被保存到xml的文件中,例如還是使用上例中Students的例子,可能保存的文件內容如下:

Xml代碼  收藏代碼
  1. <?xml version="1.0" encoding="UTF-8" standalone="yes"?>  
  2. <ns2:students xmlns:ns2="http://www.liulutu.com/students/">  
  3.     <student sex="Female" name="Bob" birthday="2013-11-27+08:00" />  
  4. </ns2:students>  

 假設,默認的值就是Female,那就可以省略sex的設置,期望的內容就變成了:

Xml代碼  收藏代碼
  1. <?xml version="1.0" encoding="UTF-8" standalone="yes"?>  
  2. <ns2:students xmlns:ns2="http://www.liulutu.com/students/">  
  3.     <student name="Bob" birthday="2013-11-27+08:00" />  
  4. </ns2:students>  

在繼續之前,有一點需要說明一下就是:如果想某項內容不輸入,則可以通過設置該項內容爲空來實現。

所以要想讓Female不輸入,則需要想辦法讓它變成空。

 

方法一:使用public void beforeMarshal(Marshaller m)方法

可以在StudentType裏定義public void beforeMarshal(Marshaller m)方法,其中方法內容爲:

Java代碼  收藏代碼
  1. public void beforeMarshal(Marshaller m) {  
  2.     if (SexType.FEMALE == this.sex) {  
  3.         this.sex = null;  
  4.     }  
  5. }  

這樣修改的一個潛在問題就是在這之後再讀取sex變量,值可能就是空的,因爲也需要修改getSex()方法,讓它在null值的情況下返回一個默認值:

Java代碼  收藏代碼
  1. public SexType getSex() {  
  2.     return sex==null?SexType.FEMALE:sex;  
  3. }  

 或者引入一箇中間變量,就是不直接存儲sex (把它標記成,而是引入一個dummySex,這個對象外界修改不了,只在保存的時候纔有用,例如: 

Java代碼  收藏代碼
  1. @XmlAttribute(name = "sex")  
  2. protected SexType dummySex = null;  
  3.   
  4. @XmlTransient  
  5. protected SexType sex;  

 

Java代碼  收藏代碼
  1. public void beforeMarshal(Marshaller m) {  
  2.     if (SexType.FEMALE != this.sex) {  
  3.         this.dummySex = this.sex;  
  4.     }  
  5. }  

 不過這樣一來,就需要在解析的時候做dummySex到sex的映射(使用unmarshal()方法可以做到),和上面相比,沒什麼改進。

 

根據上面的說明:只有值是空的時候纔不保存,那對於像int,float之類的primitive類型就有問題了,因爲他們沒有辦法設置爲null,一個可能的變通方法就是將這些primitive對象類型定義爲他們的wrap類,例如Integer,Float等,然後就可以解決這個問題了。

 

方法二:使用Marshaller.Listener

同方法一類型,只是把各個模型中定義beforeMarshal()方法變成定義在監聽事件裏。

 

方法三:使用XmlAdapter

上面介紹的方法都有一定的缺點:要麼要把內容設置爲空;要麼要引入某種中介變量。實際上,在保存之後,所以有內容都可以認爲是一個普通的字符串,所以可以想辦法把所以的內容改變成某個符串再輸入就可以了,然後根據需求,決定是轉成一個有意義的字符串還是一個null字符,如果是null字符串,則結果就不會被保存。

 

因爲我們可以使用XmlAdapter來實際從某個字符到某個類型的轉換,如上描述,最簡單的就是所有的內容都轉換成或null的字符串,或類型對應的字符串內容。例如從上面的SexType轉成字符串的XmlAdapter的實現:

Java代碼  收藏代碼
  1. import javax.xml.bind.annotation.adapters.XmlAdapter;  
  2.   
  3. public class SexTypeXmlAdapter extends XmlAdapter<String, SexType> {  
  4.   
  5.     @Override  
  6.     public String marshal(SexType v) throws Exception {  
  7.         if (v == null || v == SexType.FEMALE) {  
  8.             return null;  
  9.         }  
  10.         return v.name();  
  11.     }  
  12.   
  13.     @Override  
  14.     public SexType unmarshal(String v) throws Exception {  
  15.         if (v == null) {  
  16.             return SexType.FEMALE;  
  17.         }  
  18.         return SexType.valueOf(v);  
  19.     }  
  20.   
  21. }  

 同樣的,只有在不是FEMALE的時候才保存。不過這裏用的是XmlAdapter來實現的,所以再也不需要beforeMarshal()方法,也不需要引入中介變量。不過我需要告訴JAXB,說在sex變量上給我應用這個轉換,這個通過在sex變量上添加以下聲明實現:

Java代碼  收藏代碼
  1. @XmlAttribute(name = "sex")  
  2. @XmlJavaTypeAdapter(SexTypeXmlAdapter.class)  
  3. protected SexType sex;  

 這樣就實現我們的意圖。

 

關於XmlAdapter的更多內容:

看看XmlJavaTypeAdapter的聲明:

Java代碼  收藏代碼
  1. @Retention(RUNTIME) @Target({PACKAGE,FIELD,METHOD,TYPE,PARAMETER})  
  2. public @interface XmlJavaTypeAdapter  

可以看出這個annotation可以用在從包級到參數級的所有級別上,這樣的一個好外就是:假設我的類中或者包中有很多這樣SexType類型,那要一個一個添加就相當麻煩了,這個時候就可以所這個聲明加在更高一級的對象上,例如加在類級別上,則所有該類中的此類對象都自動應用。

 

@XmlJavaTypeAdapters

除此之外,還有另一個annotation @XmlJavaTypeAdapters,從名字上可以看出它是XmlJavaTypeAdapter的複數形式,就是說可以包含多個@XmlJavaTypeAdapter聲明:

Java代碼  收藏代碼
  1. @Retention(RUNTIME) @Target({PACKAGE})  
  2. public @interface XmlJavaTypeAdapters   

從聲明中可以看出,這個只能用在包級別上,例如:

Java代碼  收藏代碼
  1. @XmlJavaTypeAdapters({@XmlJavaTypeAdapter(ItemTypeValueAdapter.class),@XmlJavaTypeAdapter(BooleanValueAdapter.class), @XmlJavaTypeAdapter(StringValueAdapter.class), @XmlJavaTypeAdapter(IntValueAdapter.class)})  
  2. package cn.com.bjfanuc.assessment.models;  
  3.   
  4. import javax.xml.bind.annotation.adapters.*;  

 不過這裏有一個問題:怎麼在包級別上使用這些annotation呢?這就涉及到package-info.java文件了。

 

pacakge-info.java

這個文件是一個特殊的java文件,通常用來聲明一些和包相關的信息,不能含有公共或私有類聲明,關於它的介紹,可以參考http://strong-life-126-com.iteye.com/blog/806246 .

上面的@XmlJavaTypeAdapters就需要定義在這樣的一個文件裏。其實包名和引用的依賴需要聲明。

 

CDATA

如果想把某個字段保存成cdata類型,我沒發現什麼好方法,好像總是需要做某種特殊處理,否則< 或 >會被轉義。

例如,我們想name保存成以下格式:

Xml代碼  收藏代碼
  1. <name><![CDATA[ bob ]]></name>  

首先修改一下name的annotation,從attribute改成element

Java代碼  收藏代碼
  1. @XmlElement(name="name")  
  2. protected String name;  

然後要做的就怎麼讓它變成一個CDATASection,方法之一就是如上面所示,用XmlAdapter來做,如下:

Java代碼  收藏代碼
  1. public class CDATASectionAdapter extends XmlAdapter<String, String> {  
  2.   
  3.     @Override  
  4.     public String unmarshal(String v) throws Exception {  
  5.         return v;  
  6.     }  
  7.   
  8.     @Override  
  9.     public String marshal(String v) throws Exception {  
  10.         if(v != null){  
  11.             return "<![CDATA["+v+"]]>";  
  12.         }  
  13.         return null;  
  14.     }  
  15.   
  16. }  

然後在name上使用這個adapter

Java代碼  收藏代碼
  1. @XmlElement(name="name")  
  2. @XmlJavaTypeAdapter(CDATASectionAdapter.class)  
  3. protected String name;  

這個時候,輸入的內容大致:

Xml代碼  收藏代碼
  1. <name>&lt;![CDATA[Bob]]&gt;</name>  

可以看到,最前的<和最後的>都被轉義了。因此我們需要告訴Marshaller,碰到cdata的時候不要轉義。可以通過給Marshaller設置定義的CharacterEscapeHandler屬性來實現。

首先,看看我們的自定義氣CharacterEscapeHandler類:

Java代碼  收藏代碼
  1. class CustomCharacterEscapeHandler implements CharacterEscapeHandler {  
  2.     private int cdataMiniLength = "<![CDATA[]]>".length();  
  3.     private static String[] escapeList = new String[63];  
  4.     static {  
  5.         escapeList[(int'&'] = "&amp;";  
  6.         escapeList[(int'<'] = "&lt;";  
  7.         escapeList[(int'>'] = "&gt;";  
  8.         escapeList[(int'"'] = "&quot;";  
  9.         escapeList[(int'\t'] = "&#x9;";  
  10.         escapeList[(int'\r'] = "&#xD;";  
  11.         escapeList[(int'\n'] = "&#xA;";  
  12.     }  
  13.   
  14.   
  15.     public void escape(char[] ch, int start, int length,  
  16.             boolean isAttVal, Writer out) throws IOException {  
  17.         if (isAttVal) {  
  18.             for (char c : ch) {  
  19.                 if (escapeList.length > ((int) c)  
  20.                         && escapeList[(int) c] != null) {  
  21.                     out.write(escapeList[(int) c]);  
  22.                 } else {  
  23.                     out.write(c);  
  24.                 }  
  25.             }  
  26.         } else {  
  27.             if (length >= cdataMiniLength) {  
  28.                 String s = new String(ch).trim();  
  29.                 if (s.startsWith("<![CDATA[")  
  30.                         && s.endsWith("]]>")) {  
  31.                     out.write(ch);  
  32.                     return;  
  33.                 }  
  34.             }  
  35.             for (char c : ch) {  
  36.                 if (c == '"' || c == '\t' || c == '\n') {  
  37.                     out.write(c);  
  38.                 } else if (escapeList.length > ((int) c)  
  39.                         && escapeList[(int) c] != null) {  
  40.                     out.write(escapeList[(int) c]);  
  41.                 } else {  
  42.                     out.write(c);  
  43.                 }  
  44.             }  
  45.         }  
  46.     }  
  47.   
  48. }  
  49.    

這個類定義了自己的轉義方法,並且attribute和非attribute的轉義字符數還不一樣。另外就是當碰到以  <![CDATA[ 開頭和 ]]> 結尾的串時不做轉義。

 

最後就是把這個自定義的類應用到Marshaller上去:

Java代碼  收藏代碼
  1. marshaller.setProperty(CharacterEscapeHandler.class.getName(),  
  2.         new CustomCharacterEscapeHandler());  

 最後,再運行輸入如下:

Xml代碼  收藏代碼
  1. <name><![CDATA[Bob]]></name>  

 對於Unmarshall那端,不需要做什麼修改,可以測試一下:

Java代碼  收藏代碼
  1. JAXBContext context = JAXBContext.newInstance(Students.class);  
  2. Unmarshaller unmarshaller = context.createUnmarshaller();  
  3. Students model = (Students) unmarshaller.unmarshal(new File("a.xml"));  
  4. List<StudentType> student = model.getStudent();  
  5. for(StudentType st: student){  
  6.     System.out.println(st.getName());  
  7. }  

 

發佈了14 篇原創文章 · 獲贊 19 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章