現在參與的項目是一個純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;
@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>
<type>calendar</type>
<filename>calendar.2012.11.29.dat</filename>
</input-files>
</request>
可惜這個世界不會那麼清淨,這個格式有些時候貌似并不符合要求,比如request-time的格式、input-files的格式,我們實際需要的格式是這樣的:
<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>
對不同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 {
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);
public object fromstring(string str) {
method method = getcustomenumstaticvalueofmethod();
return enhancedfromstring(str);
return method.invoke(null, str);
private method getcustomenumnamemethod() {
return enumtype.getmethod(custom_enum_name_method, (class<?>[])null);
return null;
private method getcustomenumstaticvalueofmethod() {
method method = enumtype.getmethod(custom_enum_value_of_method, (class<?>[])null);
if(method.getmodifiers() == modifier.static) {
return method;
private object enhancedfromstring(string str) {
return super.fromstring(str);
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);
if(formatter == null) {
return formatter.format(obj);
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類似:
@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 {
public product(string 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"/>
</trades>
作為資料檔案,對java類的定義顯然是不合理的,因而簡單一些,我們可以編寫自己的converter将class屬性從product中去除:
xstream.registerconverter(new productconverter(
xstream.getmapper(), xstream.getreflectionprovider()));
public productconverter(mapper mapper, reflectionprovider reflectionprovider) {
super(mapper, reflectionprovider);
public boolean canconvert(@suppresswarnings("rawtypes") class type) {
return product.class.isassignablefrom(type);
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輸出結果為:
<product name="fx" ratio="0.59"/>
<product name="future" maturity="2.123"/>
然而如果有人希望xml的輸出結果如下呢?
<fx ratio="0.59"/>
<future maturity="2.123"/>
大概找了一下,可能可以定義自己的mapper來解決,不過xstream的源碼貌似比較複雜,沒有時間深究這個問題,留着以後慢慢解決吧。
補充:
對map類型資料,xstream預設使用以下格式顯示:
<map class="linked-hash-map">
<entry>
<string>key1</string>
<string>value1</string>
</entry>
<string>key2</string>
<string>value2</string>
</map>
但是對一些簡單的map,我們希望如下顯示:
<map>
<entry key="key1" value="value1"/>
<entry key="key2" value="value2"/>
對這種需求需要通過編寫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();
@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">
在xstream中,如果定義的字段是一個父類或接口,在序列化是會預設加入class屬性以确定反序列化時用的類,為了去掉這個class屬性,可以定義預設的實作類來解決(雖然感覺這種解決方案不太好,但是目前還沒有找到更好的解決方案)。
xstream.adddefaultimplementation(linkedhashmap.class, map.class);