【问题标题】:How can I deserialize the object, if it was moved to another package or renamed?如果对象被移动到另一个包或重命名,我该如何反序列化它?
【发布时间】:2010-03-01 20:32:03
【问题描述】:

考虑以下情况:

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

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

【问题讨论】:

    标签: java serialization


    【解决方案1】:

    有可能:

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

    这也允许人们忽略 serialVersionUIDs 不匹配,甚至在其字段结构发生更改时反序列化一个类。

    【讨论】:

    • 感谢您的 cmets,+1。确实,这可以是一个解决方案,但我对反序列化的模块没有影响。所以我只能欺骗.ser文件,或者序列化的类。
    • 这应该是公认的答案。它的工作原理,只需使用这个 HackedObjectInputStream 来读取对象
    • 我在尝试使用它时遇到了一些灵​​活性问题。所以这里是我的版本,如果你需要的话stackoverflow.com/a/14608062/1085787
    • !小心!用这个!。如果序列化文件中的“oldPackage.clazz”与“newPackage.clazz”的结构或变量不完全相同,您将得到“StreamCorruptedException:错误格式X”。因此,仅当序列化文件完全代表 newPackage.clazz 时才使用它!
    • 如果我不使用 'ObjectInputStream' 而是 kryo.register(... new serilizer(..){..read..write..}) 怎么办?我看不到任何可以覆盖的 readClassDescriptor。
    【解决方案2】:

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

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

    编辑:如果您控制反序列化过程,实际上有一个钩子允许这样做,请参阅其他答案。

    可以使用 readResolve() 来 从移动/重命名中恢复 班级?如果不是,请解释原因。

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

    【讨论】:

    • 我同意这个答案,我还想补充一点,如果返回的类与最初要求的全名不同,覆盖 ObjectInputStream#resolveClass(ObjectStreamClass) 将无济于事(就是这种情况)。
    • @dma_k:实际上在我看来,您可以通过覆盖该方法来实现您想要的效果
    • Comment from Igor 实际上显示了解决方案,但事实证明我无法欺骗反序列化过程,而只能欺骗模型。
    • 他不能以某种方式使用反射吗?
    • @dabicho:如果只涉及一个类,这种方式就没有意义。您不能使用反射来临时获得类的包或类似的东西。但是,如果您想要一个可重用的通用解决方案,您可以使用反射和自定义注释让类说“我是 com.oldpackage.OldClass 的反序列化目标”,并让反序列化过程自动选择它。但我怀疑这个问题在任何项目中出现的频率足以证明这种努力是合理的。
    【解决方案3】:

    如果您使用 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”的字符数。

    当然你可以制作一个补丁来自动更新它。

    【讨论】:

    • Cygnus Hex Editor 是否允许用另一个字符串(不同长度)替换一个字符串?
    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

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

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

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

      【讨论】:

      • +1 用于将readResolve() 注入“旧”类的提示。但我假设我的问题是,已经考虑恢复旧包中的类,我要求替代方案。
      【解决方案6】:

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

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

      AC ED

      协议版本号

      对象数据

      对象的类描述

      类描述有下一个格式:

      类名

      序列版唯一 ID(SHA1 来自 字段和方法签名)

      序列化选项

      字段描述符

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

      【讨论】:

      • 我想,你的意思是object dataobject's class description 之后在流中。感谢您的回答!
      • 不,正如我在 Horstmann 的“Core Java Volume I”中读到的,对象数据排在第一位。
      【解决方案7】:

      增加十六进制编辑方式。

      它对我有用,用新的包名替换旧包名比实现覆盖 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
      

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-07-29
        • 2011-02-03
        • 1970-01-01
        • 2016-09-16
        • 1970-01-01
        • 2013-10-29
        相关资源
        最近更新 更多