Java序列化 - 自动处理更改的字段?

时间:2013-05-16 22:41:06

标签: java serialization

更新5月20日:我应该提到有问题的对象确实设置了“serialVersionUID”(旧的和新的都是相同的值),但是在调用readObject()之前序列化失败,但有以下异常:

Exception in thread "main" java.io.InvalidClassException: TestData; incompatible types for field number

我现在还在下面加一个例子。

我正在使用一个大型应用程序,它从客户端到服务器发送序列化对象(实现Serializable,而不是Exernalizable)。不幸的是,我们现在的情况是现有字段完全改变了类型,这打破了序列化。

计划首先升级服务器端。从那里,我可以控制序列化对象的新版本,以及ObjectInputStream来读取来自客户端的(最初的旧)对象。

我起初想过在新版本中实现readObject();然而,在尝试之后,我发现在调用方法之前验证失败(由于不兼容的类型)。

如果我是ObjectInputStream的子类,我可以完成我想要的吗?

更好的是,是否有任何第三方库可以进行任何类型的“魔术”?如果有任何工具/库可以将序列化的对象流转换为类似HashMaps的数组,那将非常有趣......无需自己加载对象。我不确定是否可以这样做(将序列化对象转换为HashMap而不加载对象定义本身),但如果可能的话,我可以设想一个可以转换序列化对象(或对象流)的工具)使用一组属性转换提示/规则等新版本...

感谢您的任何建议。

5月20日更新 - 下面的示例源 - TestData中的字段'number'从旧版本中的'int'变为新版本中的'Long'。注意在新版本的TestData中没有调用readObject(),因为在它到达之前会抛出异常:

线程“main”中的异常java.io.InvalidClassException:TestData;字段编号不兼容的类型

以下是来源。将其保存到文件夹,然后创建子文件夹“旧”和“新”。将TestData类的“旧”版本放在“old”文件夹中,将新版本放在“new”中。将“WriteIt”和“ReadIt”类放在main(旧/新的)父文件夹中。 'cd'到'old'文件夹,并使用:javac -g -classpath . TestData.java进行编译 ...然后在'new'文件夹中执行相同的操作。 'cd'回到父文件夹,并编译WriteIt / ReadIt:

javac -g -classpath .;old WriteIt.java

javac -g -classpath .;new ReadIt.java

然后运行:

java -classpath .;old WriteIt  //Serialize old version of TestData to TestData_old.ser

java -classpath .;new ReadIt  //Attempt to read back the old object using reference to new version

[旧版本] TestData.java

import java.io.*;


public class TestData
    implements java.io.Serializable
{
    private static final long serialVersionUID = 2L;

    public int number;
}

[新版本] TestData.java

import java.io.*;


public class TestData
    implements java.io.Serializable
{
    private static final long serialVersionUID = 2L;

    public Long number; //Changed from int to Long


    private void readObject(final ObjectInputStream ois)
        throws IOException, ClassNotFoundException
    {
        System.out.println("[TestData NEW] readObject() called...");
        /* This is where I would, in theory, figure out how to handle
         * both the old and new type. But a serialization exception is
         * thrown before this method is ever called.
         */
    }
}

WriteIt.java - 将旧版TestData序列化为'TestData_old.ser'

import java.io.*;

public class WriteIt
{
    public static void main(String args[])
        throws Exception
    {
        TestData d = new TestData();

        d.number = 2013;

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("TestData_old.ser"));

        oos.writeObject(d);

        oos.close();

        System.out.println("Done!");
    }
}

ReadIt.java - 尝试将旧对象反序列化为新版本的TestData。由于事先发生异常,未调用新版本中的 readObject()。

import java.io.*;

public class ReadIt
{
    public static void main(String args[])
        throws Exception
    {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("TestData_old.ser"));

        TestData d = (TestData)ois.readObject();

        ois.close();

        System.out.println("Number = " + d.number);
    }
}

4 个答案:

答案 0 :(得分:6)

您的直接问题似乎是您没有为您的类修复serialVersionUID字符串。如果不这样做,对象序列化和反序列化代码将根据发送和接收的类型的表示结构生成UID。如果更改一端的类型而不更改另一端的类型,则UID不匹配。如果您修复的UID,然后将读卡器代码将得到过去的UID检查,您的readObject方法将有机会在串行数据为“解决”的区别。

除此之外,我的建议是:

  • 尝试改变所有的客户端和服务器的同时的,这样你就不需要使用的readObject /黑客的writeObject

  • 如果这是不实际的,尝试为搬走在什么地方有作为系统的演进版本的可能性匹配使用Java序列化对象。使用XML,JSON或对“协议”或“架构”更改不太敏感的其他内容。

  • 如果这不可行,请修改您的客户端/服务器协议。

我怀疑你会通过继承ObjectStream类获得任何牵引力。 (看看OpenJDK代码库中的代码......)

答案 1 :(得分:3)

即使对类的更改会使旧的序列化数据与新类不兼容,如果实现Externalizable并在编写类数据之前写入指示版本的额外字段,也可以继续使用序列化。这允许readExternal以您指定的方式处理旧版本。我知道无法自动执行此操作,但使用此手动方法可能对您有用。

以下是修改后的类,它们将编译并在不抛出异常的情况下运行。

旧/ TestData.java

import java.io.*;

public class TestData
implements Externalizable
{
    private static final long serialVersionUID = 1L;
    private static final long version = 1L;

    public int number;

    public void readExternal(ObjectInput in)
    throws IOException, ClassNotFoundException {
        long version = (long) in.readLong();
        if (version == 1) {
            number = in.readInt();
        }
    }
    public void writeExternal(ObjectOutput out)
    throws IOException {
        out.writeLong(version); // always write current version as first field
        out.writeInt(number);
    }
}

新/ TestData.java

import java.io.*;

public class TestData
implements Externalizable
{
    private static final long serialVersionUID = 1L;
    private static final long version = 2L; // Changed

    public Long number; //Changed from int to Long

    public void readExternal(ObjectInput in)
    throws IOException, ClassNotFoundException {
        long version = (long) in.readLong();
        if (version == 1) {
            number = new Long(in.readInt());
        }
        if (version == 2) {
            number = (Long) in.readObject();
        }
    }

    public void writeExternal(ObjectOutput out)
    throws IOException {
        out.writeLong(version); // always write current version as first field
        out.writeObject(number);
    }
}

可以使用以下语句运行这些类

$ javac -g -classpath .:old ReadIt.java
$ javac -g -classpath .:old WriteIt.java
$ java -classpath .:old WriteIt
Done!
$ java -classpath .:old ReadIt
Number = 2013
$ javac -g -classpath .:new ReadIt.java
$ java -classpath .:new ReadIt
Number = 2013

更改类以实现Exernalizable而不是Serializable将导致以下异常。

Exception in thread "main" java.io.InvalidClassException: TestData; Serializable incompatible with Externalizable
    at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:634)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1623)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1774)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
    at ReadIt.main(ReadIt.java:10)

在您的情况下,您必须先更改旧版本以实现Externalizable,同时重新启动服务器和客户端,然后您可以在具有不兼容更改的客户端之前升级服务器。

答案 2 :(得分:1)

  

在尝试之后,我发现在调用方法之前验证失败(由于不兼容的类型)

那是因为你还没有定义serialVersionUID。这很容易解决。只需在类的版本上运行serialver工具,然后将输出粘贴到新的源代码中。您可以然后能够以兼容的方式编写您的readObject()方法。

此外,您可以考虑编写readResolve()writeReplace()方法,或者以兼容的方式定义serialPersistentFields,如果可能的话。请参阅Object Serialization Specification.

另见@StephenC提出的宝贵建议。

发布您的修改我建议您更改变量的名称及其类型。这是一个删除加上一个与序列化兼容的插入,并且只要您不希望旧的int数字读入新的Long数字(为什么Long而不是long?),它将起作用。如果确实需要,我建议在那里留下int编号并添加一个具有不同名称的新Long / long字段,并修改readObject()以将新字段设置为'number'(如果尚未通过defaultReadObject()设置它)

答案 3 :(得分:0)

如果更改字段的类型,则应更改其名称。否则你会收到你看到的错误。如果要从旧数据设置新字段名称(或在对象读取期间以其他方式计算它),您可以执行以下操作:

public class TestData
    implements java.io.Serializable
{
    private static final long serialVersionUID = 2L;

    public Long numberL; //Changed from "int number;"


    private void readObject(final ObjectInputStream ois)
        throws IOException, ClassNotFoundException
    {
        System.out.println("[TestData NEW] readObject() called...");
        ois.defaultReadObject();
        if (numberL == null) {
            // deal with old structure
            GetField fields = ois.readFields();
            numberL = fields.get("number", 0L); // autoboxes old value
        }
    }
}

请勿更改serialVersionUID的值,以上内容应该能够同时阅读TestData的新版本和旧版本。