5

我正在研究 Java 序列化机制中的不同选项,以允许我们的类结构具有灵活性以进行版本容错存储(并且提倡使用不同的机制,您无需告诉我)。

例如,如果只需要向后兼容,默认的序列化机制可以处理添加和删除字段。

然而,重命名一个类或将其移动到不同的包已被证明要困难得多。我在这个问题中发现,通过继承 ObjectInputStream 并覆盖 readClassDescriptor(),我能够进行简单的类重命名和/或移动包:

    if (resultClassDescriptor.getName().equals("package.OldClass"))
        resultClassDescriptor = ObjectStreamClass.lookup(newpackage.NewClass.class);

这对于简单的重命名很好。但是,如果您随后尝试添加或删除字段,则会收到 java.io.StreamCorruptedException。更糟糕的是,即使添加或删除了一个字段,然后重命名该类,也会发生这种情况,这可能会导致多个开发人员或多个签入出现问题。

根据我所做的一些阅读,我尝试了一些覆盖resolveClass()的方法,我们的想法是我们正确地将名称重新指向新类,而不是加载旧类本身并轰炸字段更改。但这来自对序列化机制的一些细节的非常模糊的理解,我什至不确定我是否在吠叫正确的树。

所以2个精确的问题:

  1. 为什么使用 readClassDescriptor() 重新指向类名会导致反序列化在正常、兼容的类更改时失败?
  2. 有没有办法使用 resolveClass() 或其他机制来解决这个问题并允许类同时进化(添加和删除字段)并重命名/重新打包?

我四处寻找,找不到关于 SO 的等效问题。无论如何,如果存在这样的问题,请向我指出,但请仔细阅读该问题,以免关闭我,除非另一个问题实际上回答了我的确切问题。

4

5 回答 5

9

我和你一样在灵活性方面遇到了同样的问题,我找到了方法。所以这里是我的 readClassDescriptor() 版本

    static class HackedObjectInputStream extends ObjectInputStream
{

    /**
     * Migration table. Holds old to new classes representation.
     */
    private static final Map<String, Class<?>> MIGRATION_MAP = new HashMap<String, Class<?>>();

    static
    {
        MIGRATION_MAP.put("DBOBHandler", com.foo.valueobjects.BoardHandler.class);
        MIGRATION_MAP.put("DBEndHandler", com.foo.valueobjects.EndHandler.class);
        MIGRATION_MAP.put("DBStartHandler", com.foo.valueobjects.StartHandler.class);
    }

    /**
     * Constructor.
     * @param stream input stream
     * @throws IOException if io error
     */
    public HackedObjectInputStream(final InputStream stream) throws IOException
    {
        super(stream);
    }

    @Override
    protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException
    {
        ObjectStreamClass resultClassDescriptor = super.readClassDescriptor();

        for (final String oldName : MIGRATION_MAP.keySet())
        {
            if (resultClassDescriptor.getName().equals(oldName))
            {
                String replacement = MIGRATION_MAP.get(oldName).getName();

                try
                {
                    Field f = resultClassDescriptor.getClass().getDeclaredField("name");
                    f.setAccessible(true);
                    f.set(resultClassDescriptor, replacement);
                }
                catch (Exception e)
                {
                    LOGGER.severe("Error while replacing class name." + e.getMessage());
                }

            }
        }

        return resultClassDescriptor;
    }
于 2013-01-30T15:49:30.123 回答
1

问题是 readClassDescriptor 应该告诉 ObjectInputStream 如何读取当前正在读取的流中的数据。如果你查看一个序列化的数据流,你会发现它不仅存储数据,而且还有很多关于存在哪些字段的元数据。这就是允许序列化处理简单字段添加/删除的原因。但是,当您覆盖该方法并丢弃从流返回的信息时,您将丢弃有关序列化数据中的哪些字段的信息。

认为该问题的解决方案是获取 super.readClassDescriptor() 返回的值并创建一个的类描述符,该描述符返回新的类名,否则返回旧描述符的信息。(虽然,在查看 ObjectStreamField 时,它可能比这更复杂,但这是一般的想法)。

于 2012-06-07T17:58:57.400 回答
1

这就是 writeReplace() 和 readResolve() 的用途。你让它变得比实际复杂得多。请注意,您可以在相关的两个对象中或在对象流类的子类中定义这些方法。

于 2012-06-08T21:57:05.570 回答
0

我最近遇到了同样的问题,即 StreamCorruptedException 反序列化从一个包移动到另一个包的类的对象,然后通过添加新字段以兼容的方式演变。虽然@gaponov answer 最初解决了它,但我发现以下解决方案更合适,因为它不需要弄乱类名。使用 ObjectInputStreamAdapter 的类定义了映射,内部类 ObjectInputStreamAdapter 只重新定义了 resolveClass 方法:

    public class Deserializer {

    /*
     * Mapping that stores the specific new classes to use for old serialized
     * class names in order to transform old classes to the new ones for
     * compatibility reasons
     */
    private static final Map<String, Class<?>> classMapping = new HashMap<>();

    static {
        classMapping.put("com.example.old.SomeClass",
                SomeClass.class);
        classMapping.put("com.example.old.SomeClass2",
                SomeClass2.class);
    }

    public void deserialize(byte[] bytes) {
        try (ObjectInputStream o =
                new ObjectInputStreamAdapter(new ByteArrayInputStream(bytes))) {
            Object object = o.readObject();
            /* ... */
        } catch (Exception e) {
            throw new SerializationException("Cannot deserialize", e);
        }
    }

    /*
     * Adaptor that transform old classes to the new classes for compatibility
     * reasons
     */
    private class ObjectInputStreamAdapter extends ObjectInputStream {

        public ObjectInputStreamAdapter(InputStream in) throws IOException {
            super(in);
        }

        @Override
        protected Class<?> resolveClass(ObjectStreamClass desc)
                throws IOException, ClassNotFoundException {
            Class<?> klazz = classMapping.get(desc.getName());
            if (klazz != null) {
                return klazz;
            } else {
                return super.resolveClass(desc);
            }
        }

    }

}
于 2020-01-30T16:23:15.937 回答
-1

我对类描述符的修改还不够多,但是如果您的问题只是重命名和重新打包,那么有一个更简单的解决方案。您可以简单地使用文本编辑器编辑序列化数据文件,然后将旧名称替换为新名称。它以人类可读的形式存在。例如,假设我们将它OldClass放在里面oldpackage并包含一个oldField,如下所示:

package oldpackage;

import java.io.Serializable;

public class OldClass implements Serializable
{
    int oldField;
}

现在,当我们序列化这个类的一个实例并得到这样的东西时:

¬í sr oldpackage.OldClasstqŽÇ§Üï I oldFieldxp    

现在,如果我们想将类的名称更改为NewClass并放入newpackage其中并将其字段的名称更改为newField,我只需将其重命名为文件,如下所示:

¬í sr newpackage.NewClasstqŽÇ§Üï I newFieldxp    

并为新类定义适当的 serialVersionUID。

就这样。无需扩展和覆盖。

于 2012-06-07T18:44:25.987 回答