简体   繁体   中英

How can I deserialize the object, if it was moved to another package or renamed?

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 (ie 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.

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.

I don't think it's possible to do what you want.

Format of serialization file keeps class names. In detail it has next structure:

AC ED

protocol version number

object data

object's class description

Class description has next format:

full class name

serial version unique ID (SHA1 from fields and methods signatures)

serialization options

field descriptors

When you try to deserialize object serialization mechanism compares class names first (and you don't pass this step), then it compares serialVersionUID's and only after passing these 2 steps deserializes object.

Addition to the hex editing way.

It worked for me and it was easier to replace old package name with the new ones instead of implementing class replacements overriding ObjectInputStream. Especially because there were anonymous classes as well.

Here is a script which replaces old class path with the new class path in a binary format.

Here is a content o my hexreplace.sh script:

#!/bin/bash
set -xue

OLD_STR=$(echo -n $1 | hexdump -ve '1/1 "%.2X"')
NEW_STR=$(echo -n $2 | hexdump -ve '1/1 "%.2X"')
SRC_FILE=$3
DST_FILE=$4

TMP_FILE=$(mktemp /tmp/bin.patched.XXXXXXXXXX)

[ -f $SRC_FILE ]

hexdump -ve '1/1 "%.2X"' "$SRC_FILE" | sed "s/$OLD_STR/$NEW_STR/g" | xxd -r -p > "$TMP_FILE"

mv "$TMP_FILE" "$DST_FILE"

Run

hexreplace.sh old.class.path new.class.path source_file destination_file

Script works correctly when source and destination files are the same.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM