现在参与的项目是一个纯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
);