【问题标题】:Managing several versions of serialized Java objects管理多个版本的序列化 Java 对象
【发布时间】:2011-04-10 08:08:21
【问题描述】:

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

例如:反序列化时,可能会遇到这些版本之一。

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 中处理它。

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

最好的策略是什么?

【问题讨论】:

    标签: java serialization


    【解决方案1】:

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

    我看到了两种处理方法。首先,你不应该改变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 的序列化输入。

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

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

    【讨论】:

    • 如果我们要序列化/反序列化为JSON,是不是就不能避免数据类型变化的问题呢?在 JSON 中,不同的类型不会很明显。例如 Integer 和 BigInteger 看起来是一样的。布尔值和字符串当然仍然是个问题。
    • 只是好奇,最后一个例子(即“自定义序列化是要走的路”)真的有效吗?当我尝试将字段类型从 int 更改为 long 时,我在读取版本 1 对象时得到一个 InvalidClassException: <class>; incompatible types for field <field>。我不得不切换到Externalizable 才能使字段类型发生这种变化。看来即使你提供readObject(),Java序列化代码仍然会验证类描述符是否兼容(ObjectInputStream#readNonProxyDesc()最终调用ObjectStreamClass#matchFields())。
    • 根据@Bozho 的answer:“如果字段中存在类型不匹配,反序列化机制甚至不会到达readObject(..) 方法。”因此,如果想在未来的版本中更改字段的类型,似乎需要Externalizable
    【解决方案2】:

    很遗憾,不允许更改字段类型。支持两个(十个,一百个?)不同的版本太费力了。所以你可以使用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 可能会更容易一些(如果对您的项目来说还不算太晚的话)

    【讨论】:

    • 好答案!现在还为时不晚,因为它不是一个项目......我实际上正在考虑实现和使用纯对象数据库的一些问题...... :-)
    • @wlfbck 为什么它不再起作用?我检查了文档,似乎没有任何改变/被弃用。
    • @BenediktM。删除了我的评论,我错误地查看了带有 ObjectInput 的 readExternal,它没有 readFields()。
    【解决方案3】:

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

    最好的策略是什么?

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

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

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

    【讨论】:

    • 很好的答案,尽管不是我想要的。如果我问你在哪里回答的问题,我想我会使用 Hibernate :-)
    • 我认为数据库对多个部署没有任何帮助。您必须维护 ORM 映射和一系列数据库更新脚本等等。
    【解决方案4】:

    您不必维护类的多个版本。最新版本应该足够了。具体见链接5 things you don't know about Serialization“重构序列化类”

    【讨论】:

    • 链接已损坏,响应未回答提出的问题。当架构不断发展时,“最新版本应该足够了”是没有意义的。您如何反序列化使用旧模式保存的数据?
    猜你喜欢
    • 2018-02-17
    • 1970-01-01
    • 2013-04-15
    • 1970-01-01
    • 2013-11-12
    • 1970-01-01
    • 1970-01-01
    • 2020-11-21
    • 1970-01-01
    相关资源
    最近更新 更多