robinshine has posted 5 posts at DZone. You can read more from them at their website. View Full User Profile

Migrate Serialized Java Objects with XStream and XMT

02.19.2010
| 9770 views |
  • submit to reddit

Java serialization is convenient to store state of Java objects. However, there are some drawbacks of the serialized data:

  1. It is not human readable.
  2. It is Java specific and is not exchangable with other programming languages.
  3. It is not migratable if fields of associated Java class have been changed.

These drawbacks make Java serialization not a practical approach to store object states for real world projects. In a product developed recently, we use XStream to serialize/deserialize Java objects, which solves the first and second problem. The third problem is addressed with XMT, an open source tool developed by us to migrate XStream serialized XMLs. This article introduces this tool with some examples.

XStream deserialization problem when class is evolved

Assume a Task class below with a prioritized field indicating whether it is a prioritized task:

package example;

public class Task {
public boolean prioritized;
}

With XStream, we can serialize object of this class to XML like below:

import com.thoughtworks.xstream.XStream;

public class Test {
public static void main(String args[]) {
Task task = new Task();
task.prioritized = true;
String xml = new XStream().toXML(task);
saveXMLToFileOrDatabase(xml);
}

private static void saveXMLToFileOrDatabase(String xml) {
// save XML to file or database here
}
}

The resulting XML will be:

<example.Task>
<prioritized>true</prioritized>
</example.Task>

And you can deserialize the XML to get back task object:

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

public class Test {
public static void main(String args[]) {
String xml = readXMLFromFileOrDatabase();
Task task = (Task) new XStream(new DomDriver()).fromXML(xml);
}

private static String readXMLFromFileOrDatabase() {
// read XML from file or database here
}
}

Everything is fine. Now we find that a prioritized flag is not enough, so we enhance the Task class to be able to distinguish between high priority, medium priority and low priority:

package example;

public class Task {
enum Priority {HIGH, MEDIUM, LOW}

public Priority priority;
}

However deserialization of previously saved xml is no longer possible since the new Task class is not compatible with previous version.

How XMT address the problem

XMT comes to rescue: it introduces class VersionedDocument to version serialized XMLs and handle the migration. With XMT, serialization of task object can be written as:

package example;
import com.pmease.commons.xmt.VersionedDocument;

public class Test {
public static void main(String args[]) {
Task task = new Task();
task.prioritized = true;
String xml = VersionedDocument.fromBean(task).toXML();
saveXMLToFileOrDatabase(xml);
}

private static void saveXMLToFileOrDatabase(String xml) {
// save XML to file or database here
}

}

For task class of the old version, the resulting XML will be:

<example.Task version="0">
<prioritized>true</prioritized>
</example.Task>

Compared with the XML generated previously with XStream, an additional attribute version is added to the root element indicating version of the XML. The value is set to "0" unless there are migration methods defined in the class as we will introduce below.

When Task class is evolved to use enum based priority field, we add a migrate method like below:

package example;

import java.util.Stack;
import org.dom4j.Element;
import com.pmease.commons.xmt.VersionedDocument;

public class Task {
enum Priority {HIGH, MEDIUM, LOW}

public Priority priority;

@SuppressWarnings("unused")
private void migrate1(VersionedDocument dom, Stack<Integer> versions) {
Element element = dom.getRootElement().element("prioritized");
element.setName("priority");
if (element.getText().equals("true"))
element.setText("HIGH");
else
element.setText("LOW");
}
}

Migration methods need to be declared as a private method with name in form of "migrateXXX", where "XXX" is a number indicating current version of the class. Here method "migrate1" indicates that current version of the Task class is of "1", and the method migrates the XML from version "0" to "1". The XML to be migrated is passed as a VersionedDocument object which implements dom4j Document interface and you may use dom4j to migrate it to be compatible with current version of the class.

In this migration method, we read back the "prioritized" element of version "0", rename it as "priority", and set the value as "HIGH" if the task is originally a prioritized task; otherwise, set the value as "LOW".

With this migration method defined, you can now safely deserialize the task object from XML:

package example;

import com.pmease.commons.xmt.VersionedDocument;

public class Test {
public static void main(String args[]) {
String xml = readXMLFromFileOrDatabase();
Task task = (Task) VersionedDocument.fromXML(xml).toBean();
}

private static String readXMLFromFileOrDatabase() {
// read XML from file or database here
}

}

The deserialization works not only for XML of the old version, but also for XML of the new version. At deserialization time, XMT compares version of the XML (recorded in the version attribute as we mentioned earlier) with current version of the class (maximum suffix number of various migrate methods), and run applicable migrate methods one by one. In this case, if a XML of version "0" is read, method migrate1 will be called; if a XML of version "1" is read, no migration methods will be called since it is already up to date.

As class keeps evolving, more migration methods can be added to the class by increasing suffix number of latest migration method. For example, let's further enhance our Task class so that the priority field is taking a numeric value ranging from "1" to "10". We add another migrate method to the Task class to embrace the change:

@SuppressWarnings("unused")
private void migrate2(VersionedDocument dom, Stack<Integer> versions) {
Element element = dom.getRootElement().element("priority");
if (element.getText().equals("HIGH"))
element.setText("10");
else if (element.getText().equals("MEDIUM"))
element.setText("5");
else
element.setText("1");
}

This method only handles the migration from version "1" to version "2", and we do not need to care about version "0" any more, since XML of version "0" will first be migrated to version "1" by calling method migrate1 before running this method.

With this change, you will be able to deserialize the task object from XML of current version and any previous versions.

This article demonstrates the idea of how to migrate field change of classes. XMT can handle much complicated scenarios, such as migrating data defined in multiple tiers of class hierarchy, addressing class hierarchy change, etc.

For more information of XMT, please visit http://wiki.pmease.com/display/xmt/Documentation+Home

Published at DZone with permission of its author, robinshine .

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Dilip Viswanath replied on Wed, 2010/11/17 - 12:53pm

How do I download XMT.  The link to your build server on the wiki page throws a java exception stack.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.