9

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

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

我现在也在下面包含一个示例。

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

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

我一开始想到在新版本中实现 readObject() ;但是,在尝试之后,我发现在调用该方法之前验证失败(由于类型不兼容)。

如果我继承 ObjectInputStream,我能完成我想要的吗?

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

感谢您的任何建议。

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

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

下面是源码。将其保存到文件夹,然后创建子文件夹“旧”和“新”。将 TestData 类的“旧”版本放在“旧”文件夹中,将新版本放在“新”文件夹中。将“WriteIt”和“ReadIt”类放在主(旧/新的父级)文件夹中。'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

4 回答 4

8

您的直接问题似乎是您没有为您的类定义固定的 serialVersionUID 字符串。如果不这样做,对象序列化和反序列化代码会根据发送和接收类型的表示结构生成 UID。如果您在一端而不是另一端更改类型,则 UID 不匹配。如果您修复了 UID,那么阅读器代码将通过 UID 检查,您的readObject方法将有机会“解决”序列化数据中的差异。

除此之外,我的建议是:

  • 尝试同时更改所有客户端和服务器这样您就不需要使用 readObject / writeObject hacks

  • 如果这不切实际,请尽量不要任何可能随着系统发展而出现版本匹配的地方使用 Java 序列化对象。使用 XML、JSON 或其他对“协议”或“模式”更改不太敏感的东西。

  • 如果这不切实际,请对您的客户端/服务器协议进行版本控制。

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

于 2013-05-16T22:56:16.257 回答
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,同时重新启动服务器和客户端,然后您可以在更改不兼容的客户端之前升级服务器。

于 2015-05-27T17:09:06.987 回答
1

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

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

此外,如果可能的话,您可以研究写作readResolve()writeReplace()方法,或者serialPersistentFields以兼容的方式定义。请参阅对象序列化规范。

另请参阅@StephenC 的宝贵建议。

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

于 2013-05-16T23:37:55.960 回答
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.

于 2013-07-19T20:59:36.083 回答