当对象具有不同的serialVersionUID时,如何反序列化保存在数据库中的对象

Posted

技术标签:

【中文标题】当对象具有不同的serialVersionUID时,如何反序列化保存在数据库中的对象【英文标题】:How to deserialize an object persisted in a db now when the object has different serialVersionUID 【发布时间】:2010-10-22 04:21:45 【问题描述】:

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

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

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

显然,serialVersionUID 为该对象保留了 2 个不同的值,因此我想读取数据,尝试使用一个值,如果失败则尝试使用另一个值,为此我尝试更改类的serialVersionUID 使用 the ASM api。我已经能够更改该值,但问题是如何在类上进行更改,因此当它被反序列化时,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 的其他对象,最后一个值为 如果我没有为本地类指定值,则会生成一个。

-如果我没有为 serialVersionUID 指定值,则使用默认值 (8452040881660460728L),但无法取消对那些 具有其他值,则会引发错误,说明属性属于其他类型。

-如果我指定值 -5239021592691549158L,那么类将保持该值 被成功反序列化,但不是其他,相同的类型错误。

这是错误跟踪:

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

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

谢谢

【问题讨论】:

这里有一个类似的问题***.com/questions/444909/… 是的,我之前读过那个问题,但我没有找到答案,丹尼尔说他找到了解决方法,但他没有详细说明如何解决,似乎是我需要的,但我没有找到办法问他:S 【参考方案1】:

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;
    

【讨论】:

非常感谢!这帮助我恢复了大量数据! @Bhushan Bhangale,嗨,当我尝试你的答案时,java.io.StreamCorruptedException: invalid type code: 00 come in my code @Tenacious 那么你的你的代码肯定有问题,因为上面的代码按设计工作。 @Bhushan Bhangale 为什么要创建异常而不抛出异常?一种相当可疑的编码风格。 我发现这个解决方案在一段时间内非常有用,但最近它让我很头疼。 serialVersionUID 发生了变化,因为我向一个嵌套对象添加了一个字段。在反序列化期间,它总是由于 ClassCastException 而失败,因为 localClassDescriptor 无法正确处理旧数据。我现在通过使用反射将 resultClassDescriptor 上的 suid 替换为 localClassDescriptor 中的 suid 来解决此问题。这现在可以正确地反序列化数据库中的旧数据。以防万一其他人绊倒了这个【参考方案2】:

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

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

这有点乏味,但很简单。

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

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

【讨论】:

好吧,听起来不错,这是我会尝试的一些事情,如果只有 2 个版本,这可能是解决方案并且很容易,但我想知道如何更改当我发现无法使用当前值反序列化时,动态 ID,无论如何我将尝试保存一些元素的值如何失败并使用该值重复该过程,以任何方式使用列flag 将避免处理已经反序列化的元素,我会尝试让您知道,谢谢 erickson【参考方案3】:

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

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

serialVersionUIDs 的不同可能是由于 javac 生成了不同的合成成员。将警告放在前面并输入serialVersionUID

【讨论】:

我的客户端所做的更改是添加新属性,例如,在这种情况下他们没有,具体问题是缺少初始serialVersionUID,你真的认为会有如果只添加属性,即使使用 XMLEncoder 也会出现更多问题?,使用 ORM 工具将是一个很好的解决方案,我会使用休眠持久化整个对象并制作一个完整的模式来解决这个问题,但我认为客户端不会给更多时间:S,我将阅读 ObjectInputStream 代码以尝试破解以反序列化。谢谢汤姆。 XMLEncoder 和 Java 序列化在添加字段时的行为非常相似。序列化的一大优势是它允许在对象内完成自定义序列形式(包括处理遗留序列化形式)。查看 java.beans 的源代码,了解一些用于序列化标准类的丑陋技巧。【参考方案4】:

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

(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)。所以在我开始反序列化之前,如果我想把这个序列版本号改成别的东西,那么我需要在偏移量 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();

【讨论】:

a) 这是我已经做过的测试,它适用于持久化对象的值与我指定硬编码 serialVersionUID 的值相同的记录,但还有其他具有不同 serialVersionUID 值的记录,这解决方案意味着我将不得不读取每条记录,尝试反序列化,如果失败稍后再试(使用不同的序列 versionUIDvalue 重新编译)等等..直到所有记录都被迁移(编码为 xml)但有超过 4 百万记录数:S 并且如果有超过 2 个版本的 serialVersionID.. 不是解决方案,无论如何都会坦克 b) 听起来很有趣,我尝试更改类,以便 ObjectInputStream 使用的实例使用我动态指定的 serialVersionID。但是..通过找到serialVersionUID并更改它来更改字节流..我该怎么做?指定与更改的对象相同的值会很有用......任何例子?谢谢 Jorge,我想他是说如果只有两个值,你可以用第一个序列号处理一半的数据库,然后改变类来处理后半部分? 是的,舰长,我是这么理解的,但不确定这个班只有两个版本,但我会通过这样的测试来确定这一点,坦克【参考方案5】:

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

【讨论】:

【参考方案6】:

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

【讨论】:

以上是关于当对象具有不同的serialVersionUID时,如何反序列化保存在数据库中的对象的主要内容,如果未能解决你的问题,请参考以下文章

serialVersionUID的作用以及IDEAEclipse如何自动生成serialVersionUID

为什么对象序列化要定义serialVersionUID

SerialVersionUID Java - SerialVersionUID 如何工作? [复制]

关于serialVersionUID的说明

可序列化对象为什么要定义serialversionUID值?

serialVersionUID的作用