How can I deserialize the object, if it was moved to another package or renamed?
It is possible:
class HackedObjectInputStream extends ObjectInputStream {
public HackedObjectInputStream(InputStream in) throws IOException {
super(in);
}
@Override
protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
ObjectStreamClass resultClassDescriptor = super.readClassDescriptor();
if (resultClassDescriptor.getName().equals("oldpackage.Clazz"))
resultClassDescriptor = ObjectStreamClass.lookup(newpackage.Clazz.class);
return resultClassDescriptor;
}
}
This also allows one to ignore serialVersionUIDs mismatch or even deserialize a class if its field structure was changed.
Question: Is it possible to load the new class instances from this file using any tricks (except trivial copying the class into old package and then using the deserialization wrapper logic)?
I don't think there are any other "tricks" you could use that don't involve at least a partial reimplementation of the serialization protocol.
Edit: there is in fact a hook that allows this if you control the deserialization process, see the other answer.
It is possible to use readResolve() to recover from moving/renaming the class? If not, please, explain why.
No, because the deserialization mechanism will fail much earlier, at the stage where it tries to locate the class that's being deserialized - it has no way of knowing that a class in a different package has a readResolve()
method it's supposed to use.
If you use Cygnus Hex Editor you can manually change the name of the package/class.
If the new name (always including the package) has the same size you can just replace the old name by the new name, but if the size has changed you need to update the first 2 chars before the name with new new length.
Right click the Standard Data Types and change to Big Endian.
The length is a Signed Word.
For example:
00 0E 70 61 63 6B 61 67 65 2E 53 61 6D 70 6C 65
. . p a c k a g e . S a m p l e
is how package.Sample is writen. 00 0E means 14, the number of chars "package.Sample" has.
If we want to change to newpackage.Sample we replace that string to:
00 12 6E 65 77 70 61 63 6B 61 67 65 2E 53 61 6D 70 6C 65
. . n e w p a c k a g e . S a m p l e
00 12 means 18, the number of chars "newpackage.Sample" has.
And of course you can make a patcher to update this automatically.
Use this class instead of ObjectInputStream if your classes moved to another namespace.
class SafeObjectInputStream extends ObjectInputStream {
private final String oldNameSpace;
private final String newNameSpace;
public SafeObjectInputStream(InputStream in, String oldNameSpace, String newNameSpace) throws IOException {
super(in);
this.oldNameSpace = oldNameSpace;
this.newNameSpace = newNameSpace;
}
@Override
protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
ObjectStreamClass result = super.readClassDescriptor();
try {
if (result.getName().contains(oldNameSpace)) {
String newClassName = result.getName().replace(oldNameSpace, newNameSpace);
// Test the class exists
Class localClass = Class.forName(newClassName);
Field nameField = ObjectStreamClass.class.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(result, newClassName);
ObjectStreamClass localClassDescriptor = ObjectStreamClass.lookup(localClass)
Field suidField = ObjectStreamClass.class.getDeclaredField("suid");
suidField.setAccessible(true);
suidField.set(result, localClassDescriptor.getSerialVersionUID());
}
} catch(Exception e) {
throw new IOException("Exception when trying to replace namespace", e);
}
return result;
}
@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (desc.getName().contains(oldNameSpace)) {
String newClassName = desc.getName().replace(oldNameSpace, newNameSpace);
return Class.forName(newClassName);
}
return super.resolveClass(desc);
}
}
You may use it as follows:
ObjectInputStream objectStream = new SafeObjectInputStream(inputStream, "org.oldnamespace", "org.newnamespace");
objectStream.readObject();
It won't fail with StreamCorruptedException if some of your classes change. Instead, it will try to load as many fields as possible. You may perform data validation/upgrade by implementing readObject
method in your classes.
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// Validate read data here
}
Probably your best bet is to recreate the old class (name, package and serial ID), read in the serialized form, then copy the data to an instance of the new object and reserialize that.
If you have a lot of these serialized objects, perhaps you could write a small script to do this so the "schema change" gets done in one go.
Another option is to resurrect the old class and implement its readResolve method to return an instance of the new class (perhaps by declaring a copy constructor). Personally I think I'd go for the schema change script and then delete the old class for good.