Consider the following situation:
There is a serialization file, created by the older version of the application. Unfortunately, the package has changed for the class, that has been serialized. And now I need to load the information from this file into the same class, but located in different package. This class has serialVersionUID
defined and has not changed (i.e. is compatible).
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)? It is possible to use readResolve()
to recover from moving/renaming the class? If not, please, explain why.
The ObjectInputStream class contains readObject() method for deserializing an object.
The load() method deserializes from an open file-like object.
The Serialization is a process of changing the state of an object into a byte stream, an object is said to be serializable if its class or parent classes implement either the Serializable or Externalizable interface and the Deserialization is a process of converting the serialized object back into a copy of an object.
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With