12

My client has an oracle data base and an object was persisted as a blob field via objOutStream.writeObject, the object now has a different serialVersionUID (even though the object has no change, maybe different jvm version) and when they try to de-serialize an exception is thrown:

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

They didn't assign a fixed value for serialVersionUID since the beginning so now that some thing changed that exception is thrown. Now they don't want to loose any data, to do so I think the best is to read the objects, de-serialize them, and persist them again via XMLEncoder to avoid future errors like the current "class incompatible" error.

Apparently there are 2 different values for the serialVersionUID persisted for that object so I want to read the data, try with one value and if it fails then try with the other value, To do so I've tried to change the serialVersionUID of the class using the ASM api. I've been able to change the value but the problem is how to make active the change upon the class so when it is de-serialized the objInpStr.readObject() take my modified version of the class with my specific serializedVersionUID. I made a test class to simulate the real environment, I take an object (which has as property the object with different serialVersionUID problem) the object name is Reservation the property is 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();
    }

The test should fail with the java.io.InvalidClassException "local class incompatible" because I changed the serialVersionUID after I saved the file and used a new one to read de file but it doesn't fails so it means that the ObjectInputStream.readObject is not using my modified version of the Reservation class.

Any Ideas? Thanks in advance.

!!!!!!!!!!!!!UPDATE:

Ok, it is possible to redefine the resultClassDescriptor to override the stream serialVersionUID, but, some thing strange happens, as I said before it seems there are 2 versions of the class persisted, objects with serialVersionUID = -5239021592691549158L and others with value 8452040881660460728L this last value is the one generated if I don't specify a value to the local class.

-If I don't specify a value for the serialVersionUID then the default value (8452040881660460728L) is used, but is not possible to de-serealize the objects that has the other value, an error is thrown saying that a property is of an other type.

-If I specify the value -5239021592691549158L then classes persisted with that value are successfully de-serialized, but not the others, same error of types.

this is the error trace :

Potentially Fatal Deserialization Operation. java.io.InvalidClassException: Overriding serialized class version mismatch: local serialVersionUID = -5239021592691549158 stream serialVersionUID = 8452040881660460728 java.lang.ClassCastException: cannot assign instance of java.util.HashMap to field com.posadas.ic.rules.common.commisionRules.CommissionResult.statusCode of type java.lang.String in instance of com.posadas.ic.rules.common.commisionRules.CommissionResult

When this error was thrown the class had the value of -5239021592691549158, if change the value to 8452040881660460728 the class is successfully de-serialized, so, what happens? why is that error that tries to cast for wrong class ?

Thanks

Deleted User
  • 2,551
  • 1
  • 11
  • 18
Jorge Perez
  • 121
  • 1
  • 1
  • 6
  • Here is a similar question http://stackoverflow.com/questions/444909/java-modifying-serialversionuid-of-binary-serialized-object – Bhushan Bhangale Apr 28 '09 at 03:10
  • Yes I read that question before, but I found no answer, daniel said he found a way to solve it but he didn't detailed how, and seems to be what I need but I found no way to ask him :S – Jorge Perez Apr 28 '09 at 05:35

6 Answers6

18

Jorge I found one solution on http://forums.sun.com/thread.jspa?threadID=518416 which works.

Create the below class in your project. Whereever you creating object of ObjectInputStream, use DecompressibleInputStream instead and it deserializes the old object with the new version Id class.

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
  • 10,921
  • 5
  • 43
  • 71
  • thanks so much! this helped me recover a ton of data! – stantonk Mar 24 '14 at 16:03
  • @Bhushan Bhangale, hi when i tried your answer, java.io.StreamCorruptedException: invalid type code: 00 comes in my code – Girish Apr 28 '14 at 16:07
  • @Tenacious Then you must have a problem in *your* code, cause the code above works as designed. – wh81752 Nov 02 '14 at 16:24
  • @Bhushan Bhangale Why creating an exception and not throwing it? A rather questionable coding style. – wh81752 Nov 02 '14 at 16:26
  • I found this solution very useful for some time now but just recently it gave me a big headache. The serialVersionUID changed because I added a field to one nested Object. During deserialization it always failed due to a ClassCastException because the localClassDescriptor couldn't handle the old data correctly. I fixed this now by replacing the suid on resultClassDescriptor with the one from localClassDescriptor using reflection. This now correctly deserializes old data from the database. Just in case someone else stumbles over this – drame Aug 04 '20 at 07:43
1

I may be missing something, but it sounds like you're trying to do something more complicated than necessary. What happens if:

(a) you take the current class definition (i.e. the source code) and hard-code its serial UID to the old one (or one of the old ones), then use that class definition to deserialise the serialised instances?

(b) in the byte stream you're reading, you replace the old serial UIDs with the new one before wrapping the ObjectInputStream around them?

OK, just to clarify (b). So for example, if I have a little class like this:

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

then when the data is serialised, it looks something like this:

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

Each line is 16 bytes, and each byte is 2 hex digits. If you look carefully, on the second line, 9 bytes (18 digits) in, you'll see the serial version ID starts (1122...). So in our data here (yours will differ slightly), the offset of the serial version ID is 16 + 9 = 25 (or 0x19 in hex). So before I start deserialising, if I want to change this serial version ID to something else, then I need to write my new number at offset 25:

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

then I just proceed as normal:

ObjectInputStream oin = new ObjectInputStream(new ByteArrayInputStream(bytes));
MyClass obj = (MyClass) oin.readObject();
Neil Coffey
  • 21,615
  • 7
  • 62
  • 83
  • a) That's a test I already made, it works with records where the persisted object has the same value than the value that I specify hardcoding the serialVersionUID, but there are other records with different serialVersionUID value, this solution means I will have to read each record, try to de-serialize , if it fails try again later (recompile with different serial versionUIDvalue) and so.. till all records are migrated (encoded as xml) but there are more than 4 millions of records :S and if there are more than 2 versions of serialVersionID.. not a solution, tanks any way – Jorge Perez Apr 27 '09 at 22:34
  • b) sounds interesting, I tried to change the class so the instance that the ObjectInputStream uses is with the serialVersionID that I dinamically specify. but.. changing byte stream by finding the serialVersionUID and change it..how do I do it? it would be usefull to specify the same value than the changed object has...any example? thanks – Jorge Perez Apr 27 '09 at 22:41
  • Jorge, I think he's saying that if there are only two values, you could process half the database with the first serial id, then change the class to process the second half? – Brandon Yarbrough Apr 28 '09 at 00:27
  • Yes Captain, that's what I understood, but is not sure that there are only 2 versions of the class, but I will make my self sure of that by a test like this, tanks – Jorge Perez Apr 28 '09 at 05:16
1

If you've got multiple versions of the class stored in the database, it might be pretty tricky to deserialize and upgrade them all to a consistent serialization format in a single pass.

If possible, you might alter the table with a column to flag whether the serialized object has been processed yet. Then make passes over the table for each serialVersionUID, where you try to process any objects that haven't been dealt with yet. You can catch the InvalidClassException and go on to the next record if your updater encounters a serialized object that it doesn't handle, making a note of the version number so that you can make another pass.

This is a little tedious, but very simple.

Java serialization has some very nice features to support evolution of classes. However, you have to be aware of what you are doing. It may be that all of the objects actually have the same data, but no attention was paid to maintaining the version ID.

You could continue to use serialization once you get all of the objects updated to the same version. Just be careful as you add new fields to the class that they make sense with their default values (booleans are false, Objects are null, ints are zero, etc).

erickson
  • 265,237
  • 58
  • 395
  • 493
  • well, it doesn't sounds bad, that's some thing I'll try and may be the solution and easy if there are only 2 versions, but I would like to know how to change the value of the ID on the fly when I find that is not possible to de-serialize with the current value, any way I'll try to save some how the value of the elements failed and repeat the process with that value, any way with the column flag would avoid to process elements already de-serialized, I'll try and let you know, thanks erickson – Jorge Perez Apr 28 '09 at 05:23
1

You should be able to hack around the issue by overriding ObjectInputStream.readClassDescriptor.

Using XMLEncoder wont actually help with version migration as the compatibility rules are much the same. Really what should probably be doing is persisting the object in a relational form with the help of an ORM tool.

Probably the different serialVersionUIDs happened due to different synthetic members being generated by javac. Head the warnings and put serialVersionUID in.

Tom Hawtin - tackline
  • 145,806
  • 30
  • 211
  • 305
  • The kind of changes my client has done is add new properties for instance, in this case they didn't, the specific problem was with the lack of initial serialVersionUID, do you really think that there will be more problems even using XMLEncoder if only properties are added?, It would be a nice solution to use an ORM tool, my be with hibernate persist the entire object and make a complete schema to solve this, but I don't think the client will give more time :S, I'll read the ObjectInputStream code to try to hack around to de-serialize. thanks Tom. – Jorge Perez Apr 28 '09 at 05:43
  • XMLEncoder and Java serialisation behave much the same when adding a field. A big advantage of serialisation is that it allows custom serial forms (including dealing with legacy serialised form) to be done within the object. Look at the source to java.beans to see some ugly hacks to serialise standard classes. – Tom Hawtin - tackline Apr 28 '09 at 08:23
1

you can find the serial UID in HEX format, if you store serialized data in db, you can edit and replace the old UID with new serial UID in HEX format

jim he
  • 11
  • 1
0

Bit of a hack perhaps, but might be helpful to someone: I had a similar problem, which I solved by duplicating the offending class and settings the new class UID to 0L (for example). Then in the code which does the serialisation I copied the original object into the new object and serialised out. Then you can update your code, and deserialisation code, to use the new class in place of the old. This works perfectly, although you are stuck with the new class name. You can repeat the process, however, to recover your old class name. At the end of it, you have a fixed UID of your choosing. Tip I learned the hard way: always set your own UID !