管理多个版本的序列化Java对象

时间:2010-09-09 15:40:16

标签: java serialization

假设我有一个程序,由于某种原因需要处理旧版本的序列化对象。

例如:反序列化时,可能会遇到其中一个版本。

class Pet {
    private static final long serialVersionUID = 1L;
    int paws;
}

class Pet {
    private static final long serialVersionUID = 2L;
    long paws; // handle marsian centipedes
    boolean sharpTeeth;
}

让我们假设(逻辑上)可以使用一些聪明的策略将旧对象转换为新对象来设置不存在的字段等,但是:

如何安排我的源代码?在编写转换器时,我可能需要在同一个源代码树中使用这两个版本,但是如何在eclipse中处理它。

我是否应该在一个类加载器中进行反序列化,如果失败,请尝试使用另一个使用旧版本的类加载器(依此类推),还是有更好的方法?

什么是最好的策略?

4 个答案:

答案 0 :(得分:28)

  

让我们假设(逻辑上)可以使用一些聪明的策略将旧对象转换为新对象来设置不存在的字段等等......如何安排我的源代码?

我看到两种处理方法。首先,除非您想要serialVersionUID,否则不应更改InvalidClassException。第二条规则是更改字段的类型,而只是添加或删除序列化自动处理的字段。例如,如果序列化文件的类版本具有boolean sharpTeeth;但该类没有该字段,则在反序列化期间将忽略该类。如果反序列化的类具有sharpTeeth字段但文件没有,则在这种情况下它将初始化为其默认值false

对于要尝试同时处理向前和向后兼容性的分布式系统,这一点尤为重要。您不希望升级应用程序A的版本并破坏依赖于A的另一个应用程序B.通过不更改serialVersionUID但只添加或删除字段,您可以执行此操作。实体的更高版本需要支持旧版本而没有较新字段中的值,但较旧的实体不会介意新字段是否可用。这也意味着您也不应该更改字段的比例。

序列化非常智能,但它不处理对字段的类型更改。您不应该只是将pawsint更改为long。相反,我建议您添加long pawsLong或其他类似内容并编写代码,以处理int pawslong pawsLong具有值的可能性。

public long getPaws() {
    if (pawsLong > 0) {
        return pawsLong;
    } else {
        // paws used to be an integer
        return paws;
    }
}

您也可以编写自己的readObject方法,以便在反序列化时进行转换:

private void readObject(java.io.ObjectInputStream in) {
    super.readObject(in);
    // paws used to be an integer
    if (pawsLong == 0 && paws != 0) {
        pawsLong = paws;
    }
}

如果这对您不起作用,那么自定义序列化就是您的选择。您必须从头开始执行此操作,并使用内部版本ID定义自定义readObject(...)writeObject(...)方法。类似的东西:

// never change this
private static final long serialVersionUID = 3375159358757648792L;
// only goes up
private static final int INTERNAL_VERSION_ID = 2;
...
// NOTE: in version #1, this was an int
private long paws;

private void readObject(java.io.ObjectInputStream in) {
    int version = in.readInt();
    switch (version) {
        case 1 :
            paws = in.readInt();
            ...
        case 2 :
            paws = in.readLong();
            ...

private void writeObject(java.io.ObjectOutputStream out) {
    out.writeInt(INTERNAL_VERSION_ID);
    out.writeLong(paws);
    ...

但是这种方法对前向兼容性没有帮助。版本1阅读器无法理解版本2序列化输入。

  

我是否应该在一个类加载器中进行反序列化,如果失败,请尝试使用另一个使用旧版本的类加载器(依此类推),还是有更好的方法?

我不会建议任何这些方法。听起来很难维护。

答案 1 :(得分:11)

不幸的是,不允许更改字段类型。支持两个(十个,一百个?)不同版本将付出太多努力。因此,您可以使用readObject(ObjectInputStream in)方法。并设置固定的serialVersionUID。如果您最初没有设置它,请使用IDE或JDK serialver来获取它,这样看起来您只有一个版本的类。

如果要更改字段的类型,也请更改其名称。例如paws> pawsCount。如果字段中存在类型不匹配,则反序列化机制甚至不会使用readObject(..)方法。

对于上面的例子,一个可行的解决方案是:

class Pet implements Serializable {
    private static final long serialVersionUID = 1L;
    long pawsCount; // handle marsian centipedes
    boolean sharpTeeth;

    private void readObject(java.io.ObjectInputStream in)
        throws IOException, ClassNotFoundException {

        in.defaultReadObject();
        GetField fields = in.readFields();
        int paws = fields.get("paws", 0); // the 0 is a default value 
        this.pawsCount = paws;
    }
}

稍后添加的字段将设置为默认值。

顺便说一句,使用java.beans.XMLEncoder可能会更容易一些(如果你的项目还不算太晚)

答案 2 :(得分:1)

  

我应该在一个中进行反序列化   类加载器,如果失败尝试   使用另一个使用的类加载器   旧版本(依此类推),或者是   有更好的方法吗?

     

什么是最好的策略?

序列化确实不应该用于长期存储。

这里最好的策略是使用数据库:将对象存储在Pets表中,然后当您更改表中的字段时,所有旧数据也会更新,每个对象都有同样的,最新的架构。

这实际上是维护长期存储数据的最佳方式,并且对旧对象进行更新以填充空字段非常容易。

答案 3 :(得分:-1)

您不必维护该课程的多个版本。最新版本应该足够了。请参阅链接5 things you don't know about Serialization,特别是“重构序列化类”