简体   繁体   English

如果对象被移动到另一个包或重命名,我该如何反序列化它?

[英]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).这个类定义了serialVersionUID并且没有改变(即兼容)。

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?可以使用readResolve()从移动/重命名类中恢复吗? 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.这也允许忽略serialVersionUIDs不匹配,甚至在其字段结构发生变化时反序列化一个类。

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?可以使用 readResolve() 从移动/重命名类中恢复吗? 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.不,因为反序列化机制会更早失败,在它尝试定位正在反序列化的类的阶段 - 它无法知道不同包中的类具有它应该使用的readResolve()方法。

If you use Cygnus Hex Editor you can manually change the name of the package/class.如果您使用 Cygnus Hex Editor,您可以手动更改包/类的名称。

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.如果新名称(始终包括包)具有相同的大小,您可以用新名称替换旧名称,但如果大小已更改,则需要使用新长度更新名称之前的前 2 个字符。

Right click the Standard Data Types and change to Big Endian.右键单击标准数据类型并更改为 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.是 package.Sample 的编写方式。 00 0E means 14, the number of chars "package.Sample" has. 00 0E 表示 14,即“package.Sample”的字符数。

If we want to change to newpackage.Sample we replace that string to:如果我们想更改为 newpackage.Sample 我们将该字符串替换为:

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. 00 12 表示 18,即“newpackage.Sample”的字符数。

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.如果您的类移动到另一个命名空间,请使用此类而不是 ObjectInputStream。

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.如果您的某些类发生更改,它不会因 StreamCorruptedException 而失败。 Instead, it will try to load as many fields as possible.相反,它会尝试加载尽可能多的字段。 You may perform data validation/upgrade by implementing readObject method in your classes.您可以通过在您的类中实现readObject方法来执行数据验证/升级。

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).另一种选择是复活旧类并实现其readResolve方法以返回新类的实例(可能通过声明复制构造函数)。 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)序列版本唯一 ID(来自字段和方法签名的 SHA1)

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.当您尝试反序列化对象序列化机制时,首先比较类名(并且您没有通过此步骤),然后它会比较 serialVersionUID,并且只有在通过这两个步骤后才会反序列化对象。

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.它对我有用,用新的包名替换旧的包名更容易,而不是实现覆盖 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:这是我的hexreplace.sh脚本的内容:

#!/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.当源文件和目标文件相同时,脚本可以正常工作。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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