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