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

时间:2010-03-01 20:32:03

标签: java serialization

考虑以下情况:

有一个序列化文件,由旧版本的应用程序创建。不幸的是,已经序列化了类的包已更改。现在我需要将此文件中的信息加载到同一个类中,但位于不同的包中。此类已定义serialVersionUID并且未更改(即兼容)。

问题:是否可以使用任何技巧从该文件加载新的类实例(除了将类复制到旧包中然后使用反序列化包装逻辑)?可以使用readResolve()来恢复移动/重命名课程吗?如果没有,请解释原因。

7 个答案:

答案 0 :(得分:47)

有可能:

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;
    }
}

如果字段结构发生变化,这也允许忽略serialVersionUID不匹配甚至反序列化。

答案 1 :(得分:6)

如果您使用Cygnus Hex Editor,您可以手动更改包/类的名称。

如果新名称(总是包含包)具有相同的大小,您只需用新名称替换旧名称,但如果大小已更改,则需要使用新的新长度更新名称前的前2个字符

右键单击标准数据类型并更改为Big Endian。

长度是一个签名词。

例如:

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

是如何写入package.Sample的。 00 0E表示14,字符串“package.Sample”的数量为。

如果我们想要更改为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表示18,字符数“newpackage.Sample”具有。

当然,您可以让修补程序自动更新。

答案 2 :(得分:5)

可能你最好的办法是重新创建旧类(名称,包和序列号),以序列化形式读取,然后将数据复制到新对象的实例并重新序列化。

如果您有很多这些序列化对象,也许您可​​以编写一个小脚本来执行此操作,以便一次性完成“架构更改”。

另一个选择是恢复旧类并实现其 readResolve 方法以返回新类的实例(可能通过声明复制构造函数)。我个人认为我会选择架构更改脚本然后删除旧类。

答案 3 :(得分:5)

  

问题:是否可以加载   此文件中的新类实例   使用任何技巧(除了琐碎的   将类复制到旧包中   然后使用反序列化包装器   逻辑)?

我认为没有任何其他“技巧”可以使用,不涉及至少部分重新实现序列化协议。

  

可以使用readResolve()来实现   从移动/重命名恢复   类?如果没有,请解释原因。

不,因为反序列化机制会更早失败,在它试图找到被反序列化的类的阶段 - 它无法知道不同包中的类是否有readResolve()方法它是应该使用。

答案 4 :(得分:2)

我认为不可能做你想做的事。

序列化文件的格式保留了类名。详细说明它有下一个结构:

  

AC ED

     

协议版本号

     

对象数据

     

对象的类描述

班级描述有下一种格式:

  

全班名称

     

串行版本唯一ID(来自的SHA1)   字段和方法签名)

     

序列化选项

     

字段描述符

当您尝试反序列化对象序列化机制时首先比较类名(并且您没有通过此步骤),然后它比较serialVersionUID并且仅在通过这两个步骤后反序列化对象。

答案 5 :(得分:0)

十六进制编辑方式的补充。

它对我有用,并且用新的包名替换旧的包名要容易得多,而不是实现覆盖ObjectInputStream的类替换。尤其是因为还有匿名类。

这里是一个脚本,它将二进制格式的旧类路径替换为新类路径。

以下是我的 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"

运行

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

当源文件和目标文件相同时,脚本可以正常工作。

答案 6 :(得分:0)

如果您的类已移至另一个名称空间,请使用此类代替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);
    }
}

您可以按以下方式使用它:

ObjectInputStream objectStream = new SafeObjectInputStream(inputStream, "org.oldnamespace", "org.newnamespace");
objectStream.readObject();

如果您的某些类发生更改,它不会因StreamCorruptedException而失败。相反,它将尝试加载尽可能多的字段。您可以通过在类中实现readObject方法来执行数据验证/升级。

private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    // Validate read data here
}