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

Kar*_*rlP 37 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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

什么是最好的策略?

Gra*_*ray 30

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

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

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

序列化非常智能,但它不处理字段的类型更改.你不应该只是paws从一个int改为一个long.相反,我建议添加一个long pawsLong或一些这样的代码并编写代码来处理存在int pawslong pawsLong有价值的可能性.

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

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

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

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

// 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);
    ...
Run Code Online (Sandbox Code Playgroud)

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

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

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


Boz*_*zho 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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