Wednesday, May 16, 2007

XStream

Whenever I'm faced with a programming problem to solve, the logical first thing I do is to rummage around the 'Net, trying to find an open-source library (preferrably non-GPL, i.e CPL or BSD licensed) that already solves it. Because reinventing the wheel is costly, and most of the time the problems are such that I can't imagine to be the first to have came across them.

Few months ago, I had a requirements for passing objects between JVMs. "Doh, serialization", you'd say. Right. Except that the other people on the project felt it'd be very helpful from the diagnostics perspective if we could inspect the objects. So the logical idea - serialize using XML instead of Sun's binary serialization format. Turns out that the JavaBeans API actually provides support for something similar in the java.beans.XMLEncoder and XMLDecoder classes. It will serialize/deserialize JavaBeans using their public property getters and setters. But we really wanted something that operates on the ObjectInputStream/ObjectOutputStream idiom and serializes fields.

Eventually, I found the XStream project at CodeHaus.

The logical second thing I do when I found an open-source library is examine it. Unfortunately, lots of stuff floating out there isn't of particularly high quality, and branding also doesn't guarantee quality, be it "Apache" or "Codehaus" or anything else.

Now, after using XStream for quite a while now, I must say: well done! It delivers on its promises, the code is architecturally sound, and is insanely customizable. The generated XML is rather compact - i.e. if a value of the serialized field is exactly the same type as the declared type of the field, the type is not written out in the XML. Only if a subclass is used as the actual value. What's more, types can be aliased both on serialization and deserialization. This allowed us to ship objects from a GUI frontend JVM to a DB backend JVM, and have them being serialized/deserialized as different subclasses of a common abstract superclass hierarchy. In the GUI frontend, they were deserialized as classes with GUI operations, on the DB backend, they were deserialized as Hibernate enabled classes. Just brilliant.

I know you can do it with Sun's default serialization as well, but you must override resolveClass or replaceObject in the object input stream. Whereas with XStream, you could just configure the aliases in a factory once, and be done with it. Very elegant.

It is also possible to plug in custom serializers for certain types - i.e. we had to make sure Hibernate-specific collection classes were serialized as plain Java collection classes. It probably took me all of a 20 minutes to implement it, relying on documentation and the XStream (quite clear) source code.

Then there's a rather ingenious default mechanism for backreferencing objects. You can configure XStream to slap an "id" attribute on each element representing a serialized object, and when you need to reference it from a later written object, refer it by the ID. Anyone could come up with that, it's rather trivial. But XStream doesn't do that by default. Instead, by default it doesn't write any ID, but if a later written object needs to backreference an earlier written one, it'll use an XPath expression that selects the earlier object's element! (You even have a choice between absolute XPath or XPath relative to the referencing element.) Again, very elegant.

And to top it all, XStream is not for XML only any longer. Since the serializers are pluggable, they currently also have a JSON serializer, which sure can come in handy if you're AJAXing. (I'm not, so can't give an account of experience in this regard.)

All in all, I'm currently very happy with XStream; if you need to serialize/deserialize your Java objects to/from XML or JSON, I recommend you give it a try.

3 comments:

sparky said...

How exactly did you get Hibernate and XStream to work together? I have been trying for a while with no luck.

Thanks,

John S.

Ninju said...

I too would be interested in your solution to getting XStream to work with Hibernate retrieved objects.. I am intermittently getting the following error:

com.thoughtworks.xstream.converters.ConversionException: Cannot handle CGLIB enhanced proxies with multiple callbacks...

(Note, I have already tried disabling lazy loading, i.e. lazy="false" for all my Hibernate classes, but it did not help)

Thanx in advance,

Ninju Bohra
(ninju.bohra@gmail.com)

Attila Szegedi said...

I don't exactly remember everything I needed to do, but I know that:
1. we use Hibernate2. Maybe it was Hibernate3 that was causing us grief, but downgrading to H2 helped

2. I defined aliases for all involved classes using XStream.alias().

3. Also, we're using XStream.addDefaultImplementation to persist Hibernate-enhanced collections as normal collections:
xstream.addDefaultImplementation(net.sf.hibernate.collection.List.class, java.util.List.class);
xstream.addDefaultImplementation(net.sf.hibernate.collection.Map.class, java.util.Map.class);
xstream.addDefaultImplementation(net.sf.hibernate.collection.Set.class, java.util.Set.class);

This way, they show up on the (Hibernate-unaware) client side as ordinary Java collections.
Finally, I registered some additional converters:
Mapper mapper = xstream.getMapper();
xstream.registerConverter(new HibernateCollectionConverter(mapper));
xstream.registerConverter(new HibernateMapConverter(mapper));

which are defined as:
class HibernateCollectionConverter extends CollectionConverter
{
HibernateCollectionConverter(Mapper mapper)
{
super(mapper);
}

public boolean canConvert(Class type)
{
return super.canConvert(type) || type == List.class || type == Set.class;
}
}

and

class HibernateMapConverter extends MapConverter
{

HibernateMapConverter(Mapper mapper)
{
super(mapper);
}

public boolean canConvert(Class type)
{
return super.canConvert(type) || type == Map.class;
}
}

(Sorry for lack of formatting, Blogger comments disallow both "code" and "pre" HTML elements...

Hope that helps.