現在參與的項目是一個純Application Server,整個Server都是自己搭建的,使用JMS消息實現客戶端和服務器的交互,交互的數據格式采用XML。說來慚愧,開始為了趕進度,所有XML消息都是使用字符串拼接的,而XML的解析則是使用DOM方式查找的。我很早就看這些代碼不爽了,可惜一直沒有時間去重構,最近項目加了幾個人,而且美國那邊也開始漸漸的把這個項目開發的控制權交給我們了,所以我開始有一些按自己的方式開發的機會了。因而最近動手開始重構這些字符串拼接的代碼。
對XML到Java Bean的解析框架,熟悉一點的只有Digester和XStream,Digester貌似只能從XML文件解析成Java Bean對象,所以只能選擇XStream來做了,而且同組的其他項目也有在用XStream。一直聽說XStream的使用比較簡單,而且我對ThoughtWorks這家公司一直比較有好感,所以還以為引入XStream不會花太多時間,然而使用以后才發現XStream并沒有想象的你那么簡單。不過這個也有可能是因為我不想改變原來的XML數據格式,而之前的XML數據格式的設計自然不會考慮到如何便利的使用XStream。因而記錄在使用過程中遇到的問題,供后來人參考,也為自己以后如果打算開其源碼提供參考。廢話就到這里了,接下來步入正題。
首先對于簡單的引用,XStream使用起來確實比較簡單,比如自定義標簽的屬性、使用屬性和使用子標簽的定義等:
@XStreamAlias("request")
public class XmlRequest1 {
private static XStream xstream;
static {
xstream = new XStream();
xstream.autodetectAnnotations(true);
}
@XStreamAsAttribute
private String from;
@XStreamAsAttribute
@XStreamAlias("calculate-method")
private String calculateMethod;
@XStreamAlias("request-time")
private Date requestTime;
@XStreamAlias("input-files")
private List<InputFileInfo> inputFiles;
public static String toXml(XmlRequest1 request) {
StringWriter writer = new StringWriter();
writer.append(Constants.XML_HEADER);
xstream.toXML(request, writer);
return writer.toString();
}
public static XmlRequest1 toInstance(String xmlContent) {
return (XmlRequest1)xstream.fromXML(xmlContent);
}

@XStreamAlias("input-file")
public static class InputFileInfo {
private String type;
private String fileName;

}
public static void main(String[] args) {
XmlRequest1 request = buildXmlRequest();
System.out.println(XmlRequest1.toXml(request));
}
private static XmlRequest1 buildXmlRequest() {

}
} 對以上Request定義,我們可以得到如下結果:
<?xml version="1.0" encoding="UTF-8"?>
<request from="levin@host" calculate-method="advanced">
<request-time>2012-11-28 17:11:54.664 UTC</request-time>
<input-files>
<input-file>
<type>DATA</type>
<fileName>data.2012.11.29.dat</fileName>
</input-file>
<input-file>
<type>CALENDAR</type>
<fileName>calendar.2012.11.29.dat</fileName>
</input-file>
</input-files>
</request>
可惜這個世界不會那么清凈,這個格式有些時候貌似并不符合要求,比如request-time的格式、input-files的格式,我們實際需要的格式是這樣的:
<?xml version="1.0" encoding="UTF-8"?>
<request from="levin@host" calculate-method="advanced">
<request-time>20121128T17:51:05</request-time>
<input-file type="DATA">data.2012.11.29.dat</input-file>
<input-file type="CALENDAR">calendar.2012.11.29.dat</input-file>
</request>
對不同Date格式的支持可以是用Converter實現,在XStream中默認使用自己實現的DateConverter,它支持的格式是:yyyy-MM-dd HH:mm:ss.S 'UTC',然而我們現在需要的格式是yyyy-MM-dd’T’HH:mm:ss,如果使用XStream直接注冊DateConverter,可以使用配置自己的DateConverter,但是由于DateConverter的構造函數的定義以及@XStreamConverter的構造函數參數的支持方式的限制,貌似DateConverter不能很好的支持注解方式的注冊,因而我時間了一個自己的DateConverter以支持注解:
public class LevinDateConverter extends DateConverter {
public LevinDateConverter(String dateFormat) {
super(dateFormat, new String[] { dateFormat });
}
}
在requestTime字段中需要加入以下注解定義:
@XStreamConverter(value=LevinDateConverter.class, strings={"yyyyMMdd'T'HH:mm:ss"})
@XStreamAlias("request-time")
private Date requestTime;
對集合類,XStream提供了@XStreamImplicit注解,以將集合中的內容攤平到上一層XML元素中,其中itemFieldName的值為其使用的標簽名,此時InputFileInfo類中不需要@XStreamAlias標簽的定義:
@XStreamImplicit(itemFieldName="input-file")
private List<InputFileInfo> inputFiles;
對InputFileInfo中的字段,type作為屬性很容易,只要為它加上@XStreamAsAttribute注解即可,而將fileName作為input-file標簽的一個內容字符串,則需要使用ToAttributedValueConverter,其中Converter的參數為需要作為字符串內容的字段名:
@XStreamConverter(value=ToAttributedValueConverter.class, strings={"fileName"})
public static class InputFileInfo {
@XStreamAsAttribute
private String type;
private String fileName;

} XStream對枚舉類型的支持貌似不怎么好,默認注冊的EnumSingleValueConverter只是使用了Enum提供的name()和靜態的valueOf()方法將enum轉換成String或將String轉換回enum。然而有些時候XML的字符串和類定義的enum值并不完全匹配,最常見的就是大小寫的不匹配,此時需要寫自己的Converter。在這種情況下,我一般會在enum中定義一個name屬性,這樣就可以自定義enum的字符串表示。比如有TimePeriod的enum:
public enum TimePeriod {
MONTHLY("monthly"), WEEKLY("weekly"), DAILY("daily");
private String name;
public String getName() {
return name;
}
private TimePeriod(String name) {
this.name = name;
}
public static TimePeriod toEnum(String timePeriod) {
try {
return Enum.valueOf(TimePeriod.class, timePeriod);
} catch(Exception ex) {
for(TimePeriod period : TimePeriod.values()) {
if(period.getName().equalsIgnoreCase(timePeriod)) {
return period;
}
}
throw new IllegalArgumentException("Cannot convert <" + timePeriod + "> to TimePeriod enum");
}
}
}
我們可以編寫以下Converter以實現對枚舉類型的更寬的容錯性:
public class LevinEnumSingleNameConverter extends EnumSingleValueConverter {
private static final String CUSTOM_ENUM_NAME_METHOD = "getName";
private static final String CUSTOM_ENUM_VALUE_OF_METHOD = "toEnum";
private Class<? extends Enum<?>> enumType;
public LevinEnumSingleNameConverter(Class<? extends Enum<?>> type) {
super(type);
this.enumType = type;
}
@Override
public String toString(Object obj) {
Method method = getCustomEnumNameMethod();
if(method == null) {
return super.toString(obj);
} else {
try {
return (String)method.invoke(obj, (Object[])null);
} catch(Exception ex) {
return super.toString(obj);
}
}
}
@Override
public Object fromString(String str) {
Method method = getCustomEnumStaticValueOfMethod();
if(method == null) {
return enhancedFromString(str);
}
try {
return method.invoke(null, str);
} catch(Exception ex) {
return enhancedFromString(str);
}
}
private Method getCustomEnumNameMethod() {
try {
return enumType.getMethod(CUSTOM_ENUM_NAME_METHOD, (Class<?>[])null);
} catch(Exception ex) {
return null;
}
}
private Method getCustomEnumStaticValueOfMethod() {
try {
Method method = enumType.getMethod(CUSTOM_ENUM_VALUE_OF_METHOD, (Class<?>[])null);
if(method.getModifiers() == Modifier.STATIC) {
return method;
}
return null;
} catch(Exception ex) {
return null;
}
}
private Object enhancedFromString(String str) {
try {
return super.fromString(str);
} catch(Exception ex) {
for(Enum<?> item : enumType.getEnumConstants()) {
if(item.name().equalsIgnoreCase(str)) {
return item;
}
}
throw new IllegalStateException("Cannot converter <" + str + "> to enum <" + enumType + ">");
}
}
}
如下方式使用即可:
@XStreamAsAttribute
@XStreamAlias("time-period")
@XStreamConverter(value=LevinEnumSingleNameConverter.class)
private TimePeriod timePeriod;
對double類型,貌似默認的DoubleConverter實現依然不給力,它不支持自定義的格式,比如我們想在序列化的時候用一下格式:” ###,##0.0########”,此時又需要編寫自己的Converter:
public class FormatableDoubleConverter extends DoubleConverter {
private String pattern;
private DecimalFormat formatter;
public FormatableDoubleConverter(String pattern) {
this.pattern = pattern;
this.formatter = new DecimalFormat(pattern);
}
@Override
public String toString(Object obj) {
if(formatter == null) {
return super.toString(obj);
} else {
return formatter.format(obj);
}
}
@Override
public Object fromString(String str) {
try {
return super.fromString(str);
} catch(Exception ex) {
if(formatter != null) {
try {
return formatter.parse(str);
} catch(Exception e) {
throw new IllegalArgumentException("Cannot parse <" + str + "> to double value", e);
}
}
throw new IllegalArgumentException("Cannot parse <" + str + "> to double value", ex);
}
}
public String getPattern() {
return pattern;
}
}
使用方式和之前的Converter類似:
@XStreamAsAttribute
@XStreamConverter(value=FormatableDoubleConverter.class, strings={"###,##0.0########"})
private double value;
最后,還有兩個XStream沒法實現的,或者說我沒有找到一個更好的實現方式的場景。第一種場景是XStream不能很好的處理對象組合問題:
在面向對象編程中,一般盡量的傾向于抽取相同的數據成一個類,而通過組合的方式構建整個數據結構。比如Student類中有name、address,Address是一個類,它包含city、code、street等信息,此時如果要對Student對象做如下格式序列化:
<student name=”Levin”>
<city>shanghai</city>
<street>zhangjiang</street>
<code>201203</code>
</student>
貌似我沒有找到可以實現的方式,XStream能做是在中間加一層address標簽。對這種場景的解決方案,一種是將Address中的屬性平攤到Student類中,另一種是讓Student繼承自Address類。不過貌似這兩種都不是比較理想的辦法。
第二種場景是XStream不能很好的處理多態問題:
比如我們有一個Trade類,它可能表示不同的產品:
public class Trade {
private String tradeId;
private Product product;

}
abstract class Product {
private String name;
public Product(String name) {
this.name = name;
}

}
class FX extends Product {
private double ratio;
public FX() {
super("fx");
}

}
class Future extends Product {
private double maturity;
public Future() {
super("future");
}

} 通過一些簡單的設置,我們能得到如下XML格式:
<trades>
<trade trade-id="001">
<product class="levin.xstream.blog.FX" name="fx" ratio="0.59"/>
</trade>
<trade trade-id="002">
<product class="levin.xstream.blog.Future" name="future" maturity="2.123"/>
</trade>
</trades>
作為數據文件,對Java類的定義顯然是不合理的,因而簡單一些,我們可以編寫自己的Converter將class屬性從product中去除:
xstream.registerConverter(new ProductConverter(
xstream.getMapper(), xstream.getReflectionProvider()));
public ProductConverter(Mapper mapper, ReflectionProvider reflectionProvider) {
super(mapper, reflectionProvider);
}
@Override
public boolean canConvert(@SuppressWarnings("rawtypes") Class type) {
return Product.class.isAssignableFrom(type);
}
@Override
protected Object instantiateNewInstance(HierarchicalStreamReader reader, UnmarshallingContext context) {
Object currentObject = context.currentObject();
if(currentObject != null) {
return currentObject;
}
String name = reader.getAttribute("name");
if("fx".equals(name)) {
return reflectionProvider.newInstance(FX.class);
} else if("future".equals(name)) {
return reflectionProvider.newInstance(Future.class);
}
throw new IllegalStateException("Cannot convert <" + name + "> product");
}
}
在所有Production上定義@XStreamAlias(“product”)注解。這時的XML輸出結果為:
<trades>
<trade trade-id="001">
<product name="fx" ratio="0.59"/>
</trade>
<trade trade-id="002">
<product name="future" maturity="2.123"/>
</trade>
</trades>
然而如果有人希望XML的輸出結果如下呢?
<trades>
<trade trade-id="001">
<fx ratio="0.59"/>
</trade>
<trade trade-id="002">
<future maturity="2.123"/>
</trade>
</trades>
大概找了一下,可能可以定義自己的Mapper來解決,不過XStream的源碼貌似比較復雜,沒有時間深究這個問題,留著以后慢慢解決吧。
補充:
對Map類型數據,XStream默認使用以下格式顯示:
<map class="linked-hash-map">
<entry>
<string>key1</string>
<string>value1</string>
</entry>
<entry>
<string>key2</string>
<string>value2</string>
</entry>
</map>
但是對一些簡單的Map,我們希望如下顯示:
<map>
<entry key="key1" value="value1"/>
<entry key="key2" value="value2"/>
</map>
對這種需求需要通過編寫Converter解決,繼承自MapConverter,覆蓋以下函數,這里的Map默認key和value都是String類型,如果他們不是String類型,需要另外添加邏輯:
@SuppressWarnings("rawtypes")
@Override
public void marshal(Object source, HierarchicalStreamWriter writer,
MarshallingContext context) {
Map map = (Map) source;
for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
Entry entry = (Entry) iterator.next();
ExtendedHierarchicalStreamWriterHelper.startNode(writer, mapper()
.serializedClass(Map.Entry.class), entry.getClass());
writer.addAttribute("key", entry.getKey().toString());
writer.addAttribute("value", entry.getValue().toString());
writer.endNode();
}
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
protected void putCurrentEntryIntoMap(HierarchicalStreamReader reader,
UnmarshallingContext context, Map map, Map target) {
Object key = reader.getAttribute("key");
Object value = reader.getAttribute("value");
target.put(key, value);
}
但是只是使用Converter,得到的結果多了一個class屬性:
<map class="linked-hash-map">
<entry key="key1" value="value1"/>
<entry key="key2" value="value2"/>
</map>
在XStream中,如果定義的字段是一個父類或接口,在序列化是會默認加入class屬性以確定反序列化時用的類,為了去掉這個class屬性,可以定義默認的實現類來解決(雖然感覺這種解決方案不太好,但是目前還沒有找到更好的解決方案)。
xstream.addDefaultImplementation(LinkedHashMap.class, Map.class);