xStream

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值