添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
稳重的佛珠  ·  Camunda 7工作流引擎 API ...·  2 月前    · 
小胡子的煎饼  ·  2024 15 ...·  3 月前    · 
谦逊的红豆  ·  mp425 93.哔哩.mp4 - 副本 ...·  3 月前    · 
热心肠的红烧肉  ·  魔性论坛·  3 月前    · 
In general the OO style is to use a lot of little objects with a lot of little methods that give us a lot of plug points for overriding and variation. To do is to be -Nietzsche, To bei is to do -Kant, Do be do be do -Sinatra 使用XStream序列化、反序列化XML数据时遇到的各种问题

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

posted on 2012-11-30 01:50 DLevin 阅读(24978) 评论(3) 编辑 收藏 所属分类: 经验积累