现在参与的项目是一个纯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 utcrequest-time>
<input-files>
<input-file>
<type>datatype>
<filename>data.2012.11.29.datfilename>
input-file>
<input-file>
<type>calendartype>
<filename>calendar.2012.11.29.datfilename>
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:05request-time>
<input-file type="data">data.2012.11.29.datinput-file>
<input-file type="calendar">calendar.2012.11.29.datinput-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”>
>shanghaicity>
<street>zhangjiangstreet>
<code>201203code>
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>key1string>
<string>value1string>
entry>
<entry>
<string>key2string>
<string>value2string>
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);