12

我的客户端有一个 oracle 数据库,并且一个对象通过 objOutStream.writeObject 作为 blob 字段持久化,该对象现在有一个不同的serialVersionUID(即使对象没有变化,可能是不同的 jvm 版本),当他们尝试反序列化一个抛出异常:

java.io.InvalidClassException: CommissionResult; local class incompatible: 
 stream classdesc serialVersionUID = 8452040881660460728, 
 local class serialVersionUID = -5239021592691549158

他们从一开始就没有为它分配一个固定的值,serialVersionUID所以现在有些东西改变了,抛出了异常。现在他们不想丢失任何数据,为此我认为最好的方法是读取对象,反序列化它们,然后通过 XMLEncoder 再次持久化它们,以避免将来出现诸如当前“类不兼容”错误之类的错误。

显然,该对象的持久化有 2 个不同的值,serialVersionUID所以我想读取数据,尝试使用一个值,如果失败,则尝试使用另一个值,为此,我尝试serialVersionUID使用 ASM 接口。我已经能够更改该值,但问题是如何对类进行主动更改,因此当它被反序列化时,objInpStr.readObject()将我修改后的类的版本与我的特定serializedVersionUID. 我做了一个测试类来模拟真实环境,我取一个对象(它具有不同serialVersionUID问题的对象作为属性)对象名称是Reservation属性 CommissionResult

public class Reservation implements java.io.Serializable {


    private CommissionResult commissionResult = null;

}


public class CommissionResult implements java.io.Serializable{



}


import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.commons.SerialVersionUIDAdder;

public class SerialVersionUIDRedefiner extends ClassLoader {


    public void workWithFiles() {
        try {
            Reservation res = new Reservation();
            FileOutputStream f = new FileOutputStream("/home/xabstract/tempo/res.ser");
        ObjectOutputStream out = new ObjectOutputStream(f);

            out.writeObject(res);

            out.flush();
            out.close();

            ClassWriter cw = new ClassWriter(0); 
             ClassVisitor sv = new SerialVersionUIDAdder(cw); //assigns a real serialVersionUID 
             ClassVisitor ca = new MyOwnClassAdapter(sv); //asigns my specific serialVerionUID value
             ClassReader cr=new  ClassReader("Reservation"); 
              cr.accept(ca, 0); 

             SerialVersionUIDRedefiner   loader= new SerialVersionUIDRedefiner(); 
             byte[] code = cw.toByteArray();
             Class exampleClass =        loader.defineClass("Reservation", code, 0, code.length); //at this point the class Reservation has an especific serialVersionUID value that I put with MyOwnClassAdapter

             loader.resolveClass(exampleClass);
             loader.loadClass("Reservation");
             DeserializerThread dt=new DeserializerThread();
             dt.setContextClassLoader(loader);
             dt.run();
    } catch (Exception e) {
            e.printStackTrace();
    }}



import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class DeserializerThread extends Thread {

    public void run() {
        try {
            FileInputStream f2;

            f2 = new FileInputStream("/home/xabstract/tempo/res.ser");

             ObjectInputStream in = new ObjectInputStream(f2);


            Reservation c1 = (Reservation)in.readObject();



            System.out.println(c1);

        } catch (Exception e) {

            e.printStackTrace();
        }
        stop();
    }
}

MyOwnClassAdapter Relevant code:



public void visitEnd() {
        // asign SVUID and add it to the class

            try {

                cv.visitField(Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,
                        "serialVersionUID",
                        "J",
                        null,
                        new Long(-11001));//computeSVUID()));
            } catch (Throwable e) {
                e.printStackTrace();
                throw new RuntimeException("Error while computing SVUID for x"
                        , e);
            }


        super.visitEnd();
    }

测试应该因java.io.InvalidClassException“本地类不兼容”而失败,因为我serialVersionUID在保存文件并使用新文件读取 de 文件后更改了它,但它没有失败,因此这意味着ObjectInputStream.readObject没有使用我修改后的Reservation类版本。

有任何想法吗?提前致谢。

!!!!!!!!!!!!!更新:

好的,可以重新定义 resultClassDescriptor 以覆盖流 serialVersionUID,但是,发生了一些奇怪的事情,正如我之前所说,似乎有 2 个版本的类持久化,serialVersionUID = -5239021592691549158L 的对象和其他值为 8452040881660460728L 的对象最后value 是如果我没有为本地类指定值时生成的值。

- 如果我没有为 serialVersionUID 指定值,则使用默认值 (8452040881660460728L),但无法解除具有其他值的对象,则会引发错误,指出属性属于其他类型。

- 如果我指定值 -5239021592691549158L,则使用该值持久的类将成功反序列化,但其他类则没有,相同的类型错误。

这是错误跟踪:

可能致命的反序列化操作。java.io.InvalidClassException:覆盖序列化类版本不匹配:本地 serialVersionUID = -5239021592691549158 流 serialVersionUID = 8452040881660460728 java.lang.ClassCastException:无法将 java.util.HashMap 的实例分配给字段 com.posadas.ic.rules.common.commisionRules。 com.posadas.ic.rules.common.commisionRules.CommissionResult 实例中 java.lang.String 类型的 CommissionResult.statusCode

抛出此错误时,该类的值为-5239021592691549158,如果将值更改为8452040881660460728,则该类已成功反序列化,那么会发生什么?为什么那个错误会试图转换为错误的类?

谢谢

4

6 回答 6

18

Jorge 我在http://forums.sun.com/thread.jspa?threadID=518416上找到了一种可行的解决方案。

在您的项目中创建以下类。无论您在何处创建 ObjectInputStream 对象,请改用 DecompressibleInputStream,它会使用新版本 Id 类反序列化旧对象。

public class DecompressibleInputStream extends ObjectInputStream {

    public DecompressibleInputStream(InputStream in) throws IOException {
        super(in);
    }


    protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
        ObjectStreamClass resultClassDescriptor = super.readClassDescriptor(); // initially streams descriptor
        Class localClass = Class.forName(resultClassDescriptor.getName()); // the class in the local JVM that this descriptor represents.
        if (localClass == null) {
            System.out.println("No local class for " + resultClassDescriptor.getName());
            return resultClassDescriptor;
        }
        ObjectStreamClass localClassDescriptor = ObjectStreamClass.lookup(localClass);
        if (localClassDescriptor != null) { // only if class implements serializable
            final long localSUID = localClassDescriptor.getSerialVersionUID();
            final long streamSUID = resultClassDescriptor.getSerialVersionUID();
            if (streamSUID != localSUID) { // check for serialVersionUID mismatch.
                final StringBuffer s = new StringBuffer("Overriding serialized class version mismatch: ");
                s.append("local serialVersionUID = ").append(localSUID);
                s.append(" stream serialVersionUID = ").append(streamSUID);
                Exception e = new InvalidClassException(s.toString());
                System.out.println("Potentially Fatal Deserialization Operation. " + e);
                resultClassDescriptor = localClassDescriptor; // Use local class descriptor for deserialization
            }
        }
        return resultClassDescriptor;
    }
}
于 2009-04-28T07:15:34.863 回答
1

我可能遗漏了一些东西,但听起来你正在尝试做一些比必要的更复杂的事情。如果出现以下情况会发生什么:

(a) 您采用当前类定义(即源代码)并将其序列 UID 硬编码为旧的(或旧的之一),然后使用该类定义反序列化序列化实例?

(b) 在您正在读取的字节流中,在将 ObjectInputStream 包裹在它们周围之前,您将旧的串行 UID 替换为新的 UID?

好的,只是为了澄清(b)。例如,如果我有一个像这样的小课:

  public static class MyClass implements Serializable {
    static final long serialVersionUID = 0x1122334455667788L;
    private int myField = 0xff;
  }

然后当数据被序列化时,它看起来像这样:

ACED000573720011746573742E546573 ’..sr..test.Tes
74244D79436C61737311223344556677 t$MyClass."3DUfw
880200014900076D794669656C647870 ?...I..myFieldxp
000000FF ...ÿ

每行是 16 个字节,每个字节是 2 个十六进制数字。如果你仔细看,在第二行的 9 个字节(18 位)中,你会看到序列版本 ID 开始(1122...)。因此,在我们的数据中(您的数据会略有不同),串行版本 ID 的偏移量是 16 + 9 = 25(或十六进制的 0x19)。所以在我开始反序列化之前,如果我想将此序列版本 ID 更改为其他内容,那么我需要在偏移量 25 处写入我的新编号:

byte[] bytes = ... serialised data ...
ByteBuffer bb = ByteBuffer.wrap(bytes);
bb.putLong(25, newSerialVersionUID);

然后我照常进行:

ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(bytes));
MyClass obj = (MyClass) oin.readObject();
于 2009-04-27T22:16:43.663 回答
1

如果您在数据库中存储了该类的多个版本,则一次通过反序列化并将它们全部升级为一致的序列化格式可能会非常棘手。

如果可能,您可以使用列更改表以标记序列化对象是否已被处理。然后为 each 遍历表serialVersionUID,在其中尝试处理尚未处理的任何对象。如果您的更新程序遇到一个它无法处理的序列化对象,您可以捕获InvalidClassException并继续下一条记录,记下版本号,以便您可以再次通过。

这有点乏味,但非常简单。

Java 序列化有一些非常好的特性来支持类的进化。但是,您必须清楚自己在做什么。可能所有对象实际上都具有相同的数据,但没有注意维护版本 ID。

将所有对象更新到相同版本后,您可以继续使用序列化。在向类中添加新字段时要小心,它们的默认值是有意义的(布尔值为假,对象为空,整数为零等)。

于 2009-04-27T22:41:37.270 回答
1

您应该能够通过覆盖来解决问题ObjectInputStream.readClassDescriptor

XMLEncoder由于兼容性规则大致相同,因此使用实际上不会帮助进行版本迁移。真正应该做的是在 ORM 工具的帮助下以关系形式持久化对象。

serialVersionUID由于 javac 生成了不同的合成成员,可能会发生不同的 s。头上的警告和投入serialVersionUID

于 2009-04-28T00:24:06.820 回答
1

您可以找到 HEX 格式的串行 UID,如果您将序列化数据存储在 db 中,您可以编辑旧 UID 并将其替换为 HEX 格式的新串行 UID

于 2012-03-22T07:09:01.420 回答
0

可能有点破解,但可能对某人有帮助:我有一个类似的问题,我通过复制有问题的类并将新类 UID 设置为 0L(例如)来解决这个问题。然后在执行序列化的代码中,我将原始对象复制到新对象中并序列化。然后您可以更新您的代码和反序列化代码,以使用新类代替旧类。这很有效,尽管你被新的类名卡住了。但是,您可以重复该过程来恢复旧的类名。最后,您有一个您选择的固定 UID。提示我学到了一个艰难的方法:总是设置你自己的 UID !

于 2018-08-15T16:35:22.787 回答