用XStream和XMT迁移序列化的Java对象的教程

229 阅读5分钟

用XStream和XMT迁移序列化的Java对象

Java的序列化对于存储Java对象的状态是很方便的。然而,序列化的数据也有一些缺点:

  1. 它不是人类可读的。

  2. 它是Java特有的,不能与其他编程语言交换。

  3. 如果相关的Java类的字段被改变,它是不可迁移的。

这些缺点使得Java序列化并不是现实世界项目中存储对象状态的一种实用方法。在最近开发的一个产品中,我们使用XStream来序列化/反序列化Java对象,这解决了第一和第二个问题。第三个问题是用XMT解决的,这是我们开发的一个开源工具,用来迁移 XStream序列化的XML。这篇文章用一些例子介绍了这个工具。

计算机语言需要简化

当我们致力于将计算机语言转换为人类可以更好地理解的东西时,我们都会遇到很多问题,那就是计算机语言需要尽可能地简化。

这些语言对计算机来说是很好的,它们可以相互交流,但当人类试图参与其中时,它们就不一定会有好的结果。许多人最终会感到困惑,无法在清除这些系统方面取得多大进展。因此,有必要让它们得到清理,并使之更有用处。现在有一些人正在积极地解决这个问题,但与此同时,我们可能只是不得不处理那些不能做我们想让它们做的一切的计算机。

类进化时的XStream反序列化问题

假设下面有一个任务类,有一个优先级字段表示它是否是一个 优先级的任务

package example;

public class Task {
        public boolean prioritized;
} 

XStream,我们可以像下面这样把这个类的对象序列化为XML:

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
        }
} 

得到的 XML将是:

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

而你可以对XML进行反序列化,以取回任务对象:

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
        }
} 

一切都很好。现在我们发现一个 优先级的标志是不够的,所以我们增强了任务类,使其能够区分高优先级、中优先级和低优先级。

package example;

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

然而,由于新的任务类与以前的版本不兼容,以前保存的XML的反序列化就不再可能了。

XMT如何解决这个问题

XMT来拯救这个问题:它引入了VersionedDocument类来对序列化的XML进行版本转换,并处理迁移问题。使用XMT,任务对象的序列化可以写成:

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
        }

}

对于旧版本的任务类,产生的XML将是:

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

与以前用XStream生成的XML相比,在根元素上增加了一个额外的属性version,表示XML的版本。该值被设置为 "0",除非在类中定义有迁移方法,我们将在下面介绍。

当任务类发展到使用基于枚举的优先级字段时,我们会添加一个像下面这样的迁移方法:

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");
    }
} 

迁移方法需要被声明为一个私有方法,其名称为 "migrateXXX",其中 "XXX "是一个数字,表示类的当前版本。这里方法 "migrate1 "表示任务类的当前版本为 "1",该方法将XML从版本 "0 "迁移到 "1"。要迁移的XML是以VersionedDocument对象的形式传递的,该对象实现了dom4j Document接口,你可以使用dom4j将其迁移到与当前版本的类兼容。

在这个迁移方法中,我们读回版本 "0 "的 "优先级 "元素,将其重命名为 "优先级",如果该任务原本是一个优先级的任务,则将其值设置为 "HIGH";否则将其值设置为 "LOW"。

有了这个迁移方法的定义,你现在可以安全地从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
        }

} 

反序列化不仅对旧版本的XML有效,而且对新版本的XML也有效。在反序列化时,XMT将XML的版本(如我们前面提到的记录在版本属性中)与类的当前版本(各种迁移方法的最大后缀数)进行比较,并逐一运行适用的迁移方法。在这种情况下,如果读取的是版本为 "0 "的XML,就会调用方法migrate1;如果读取的是版本为 "1 "的XML,就不会调用任何迁移方法,因为它已经是最新的了。

随着该类的不断发展,可以通过增加最新迁移方法的后缀号来为该类添加更多的迁移方法。例如,让我们进一步增强我们的任务类,使优先级字段取一个从 "1 "到 "10 "的数字值。我们在任务类中添加另一个migrate方法来接受这种变化。

@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");
} 

这个方法只处理从版本 "1 "到版本 "2 "的迁移,我们不再需要关心版本 "0",因为在运行这个方法之前,版本 "0 "的XML将首先通过调用方法migrate1迁移到版本 "1"。

通过这一改变,你将能够从当前版本和之前任何版本的XML中反序列化任务对象。

这篇文章展示了如何迁移类的字段变化的想法。XMT可以处理很多复杂的情况,比如迁移定义在类的多层结构中的数据,解决类的层次结构变化等。