Java 8和Java 11之间的反序列化行为不同

And*_*ira 12 java serialization deserialization java-8 java-11

我在Java 11中反序列化存在问题,导致HashMap无法找到密钥。如果对这个问题有更多了解的任何人都可以说我建议的解决方法看起来还可以,或者我可以做得更好,我将不胜感激。

考虑以下人为设计的实现(实际问题中的关系稍微复杂一些,很难更改):

public class Element implements Serializable {
    private static long serialVersionUID = 1L;

    private final int id;
    private final Map<Element, Integer> idFromElement = new HashMap<>();

    public Element(int id) {
        this.id = id;
    }

    public void addAll(Collection<Element> elements) {
        elements.forEach(e -> idFromElement.put(e, e.id));
    }

    public Integer idFrom(Element element) {
        return idFromElement.get(element);
    }

    @Override
    public int hashCode() {
        return id;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof Element)) {
            return false;
        }
        Element other = (Element) obj;
        return this.id == other.id;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,我创建一个引用其自身的实例,并对其进行序列化和反序列化:

public static void main(String[] args) {
    List<Element> elements = Arrays.asList(new Element(111), new Element(222));
    Element originalElement = elements.get(1);
    originalElement.addAll(elements);

    Storage<Element> storage = new Storage<>();
    storage.serialize(originalElement);
    Element retrievedElement = storage.deserialize();

    if (retrievedElement.idFrom(retrievedElement) == 222) {
        System.out.println("ok");
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我在Java 8中运行此代码,则结果为“确定”;如果在Java 11中运行,则结果为a,NullPointerException原因是retrievedElement.idFrom(retrievedElement)return null

我在处设置了一个断点,HashMap.hash()并注意到:

  • 在Java 8中,当idFromElement要反序列化并Element(222)添加到Java 8中时,其id值为222,因此以后可以找到它。
  • 在Java 11中,id未初始化(int如果将其设为,则为0 或null Integer),将hash()其存储在中时为0 HashMap。稍后,当我尝试检索它时,它id是222,因此idFromElement.get(element)返回null

我知道这里的顺序是反序列化(Element(222))->反序列化(idFromElement)->将未完成的Element(222)放入Map中。但是,由于某种原因,id当我们到达最后一步时,在Java 8 中已经初始化,而在Java 11中则没有初始化。

我想到的解决方案是使idFromElement瞬态并编写自定义writeObjectreadObject方法,idFromElement以在之后强制反序列化id

...
transient private Map<Element, Integer> idFromElement = new HashMap<>();
...
private void writeObject(ObjectOutputStream output) throws IOException {
    output.defaultWriteObject();
    output.writeObject(idFromElement);
}

@SuppressWarnings("unchecked")
private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
    input.defaultReadObject();
    idFromElement = (HashMap<Element, Integer>) input.readObject();
}
Run Code Online (Sandbox Code Playgroud)

我能够找到的有关序列化/反序列化期间顺序的唯一参考是:

对于可序列化的类,将设置SC_SERIALIZABLE标志,字段数将对可序列化字段的数量进行计数,并在每个可序列化字段后跟一个描述符。描述符以规范顺序编写。原始类型字段的描述符首先按字段名称排序,然后写入对象类型字段的描述符(按字段名称排序)。名称使用String.compareTo排序。

这是两个相同的Java 8Java的11文档,并且似乎在暗示,基元类型的字段应该先写,所以我认为应该有没有什么区别。


实现Storage<T>完整包含:

public class Storage<T> {
    private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();

    public void serialize(T object) {
        buffer.reset();
        try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(buffer)) {
            objectOutputStream.writeObject(object);
            objectOutputStream.flush();
        } catch (Exception ioe) {
            ioe.printStackTrace();
        }
    }

    @SuppressWarnings("unchecked")
    public T deserialize() {
        ByteArrayInputStream byteArrayIS = new ByteArrayInputStream(buffer.toByteArray());
        try (ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayIS)) {
            return (T) objectInputStream.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}
Run Code Online (Sandbox Code Playgroud)

Mar*_*o13 5

正如评论中提到的并受到提问者的鼓励,以下是在版本 8 和版本 11 之间更改的代码部分,我认为这是导致不同行为的原因(基于阅读和调试)。

区别在于ObjectInputStream类,在其核心方法之一。这是 Java 8 中实现的相关部分:

private void readSerialData(Object obj, ObjectStreamClass desc)
    throws IOException
{
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;

        if (slots[i].hasData) {
            if (obj == null || handles.lookupException(passHandle) != null) {
                ...
            } else {
                defaultReadFields(obj, slotDesc);
            }
            ...
        }
    }
}

/**
 * Reads in values of serializable fields declared by given class
 * descriptor.  If obj is non-null, sets field values in obj.  Expects that
 * passHandle is set to obj's handle before this method is called.
 */
private void defaultReadFields(Object obj, ObjectStreamClass desc)
    throws IOException
{
    Class<?> cl = desc.forClass();
    if (cl != null && obj != null && !cl.isInstance(obj)) {
        throw new ClassCastException();
    }

    int primDataSize = desc.getPrimDataSize();
    if (primVals == null || primVals.length < primDataSize) {
        primVals = new byte[primDataSize];
    }
    bin.readFully(primVals, 0, primDataSize, false);
    if (obj != null) {
        desc.setPrimFieldValues(obj, primVals);
    }

    int objHandle = passHandle;
    ObjectStreamField[] fields = desc.getFields(false);
    Object[] objVals = new Object[desc.getNumObjFields()];
    int numPrimFields = fields.length - objVals.length;
    for (int i = 0; i < objVals.length; i++) {
        ObjectStreamField f = fields[numPrimFields + i];
        objVals[i] = readObject0(f.isUnshared());
        if (f.getField() != null) {
            handles.markDependency(objHandle, passHandle);
        }
    }
    if (obj != null) {
        desc.setObjFieldValues(obj, objVals);
    }
    passHandle = objHandle;
}
...
Run Code Online (Sandbox Code Playgroud)

该方法调用defaultReadFields,它读取字段的值。正如规范的引用部分所述,它首先处理原始字段的字段描述符。为这些字段读取的值在读取它们后立即设置,使用

desc.setPrimFieldValues(obj, primVals);
Run Code Online (Sandbox Code Playgroud)

重要的是:这发生它调用readObject0每个原始字段之前。

与此相反,这是 Java 11 实现的相关部分:

private void readSerialData(Object obj, ObjectStreamClass desc)
    throws IOException
{
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();

    ...

    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;

        if (slots[i].hasData) {
            if (obj == null || handles.lookupException(passHandle) != null) {
                ...
            } else {
                FieldValues vals = defaultReadFields(obj, slotDesc);
                if (slotValues != null) {
                    slotValues[i] = vals;
                } else if (obj != null) {
                    defaultCheckFieldValues(obj, slotDesc, vals);
                    defaultSetFieldValues(obj, slotDesc, vals);
                }
            }
            ...
        }
    }
    ...
}

private class FieldValues {
    final byte[] primValues;
    final Object[] objValues;

    FieldValues(byte[] primValues, Object[] objValues) {
        this.primValues = primValues;
        this.objValues = objValues;
    }
}

/**
 * Reads in values of serializable fields declared by given class
 * descriptor. Expects that passHandle is set to obj's handle before this
 * method is called.
 */
private FieldValues defaultReadFields(Object obj, ObjectStreamClass desc)
    throws IOException
{
    Class<?> cl = desc.forClass();
    if (cl != null && obj != null && !cl.isInstance(obj)) {
        throw new ClassCastException();
    }

    byte[] primVals = null;
    int primDataSize = desc.getPrimDataSize();
    if (primDataSize > 0) {
        primVals = new byte[primDataSize];
        bin.readFully(primVals, 0, primDataSize, false);
    }

    Object[] objVals = null;
    int numObjFields = desc.getNumObjFields();
    if (numObjFields > 0) {
        int objHandle = passHandle;
        ObjectStreamField[] fields = desc.getFields(false);
        objVals = new Object[numObjFields];
        int numPrimFields = fields.length - objVals.length;
        for (int i = 0; i < objVals.length; i++) {
            ObjectStreamField f = fields[numPrimFields + i];
            objVals[i] = readObject0(f.isUnshared());
            if (f.getField() != null) {
                handles.markDependency(objHandle, passHandle);
            }
        }
        passHandle = objHandle;
    }

    return new FieldValues(primVals, objVals);
}

...
Run Code Online (Sandbox Code Playgroud)

FieldValues已经引入了一个内部类。该defaultReadFields方法现在只读取字段值,并将它们作为FieldValues对象返回。然后,通过将此FieldValues对象传递给新引入的defaultSetFieldValues方法,将返回值分配给字段,该方法在内部执行desc.setPrimFieldValues(obj, primValues)最初在读取原始值后立即完成的调用。

再次强调这一点:该defaultReadFields方法首先读取原始字段值。然后它读取非原始字段值。但它是设置原始字段值之前这样做的!

这个新过程干扰了 的反序列化方法HashMap。同样,相关部分如下所示:

private void readObject(java.io.ObjectInputStream s)
    throws IOException, ClassNotFoundException {

    ...

    int mappings = s.readInt(); // Read number of mappings (size)
    if (mappings < 0)
        throw new InvalidObjectException("Illegal mappings count: " +
                                         mappings);
    else if (mappings > 0) { // (if zero, use defaults)

        ...

        Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
        table = tab;

        // Read the keys and values, and put the mappings in the HashMap
        for (int i = 0; i < mappings; i++) {
            @SuppressWarnings("unchecked")
                K key = (K) s.readObject();
            @SuppressWarnings("unchecked")
                V value = (V) s.readObject();
            putVal(hash(key), key, value, false, false);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它通过计算键的散列并使用内部putVal方法,一一读取键和值对象,并将它们放入表中。这与手动填充地图(即以编程方式填充而不是反序列化)时使用的方法相同。

Holger 已经在评论中给出了为什么这是必要的提示:无法保证反序列化键的哈希码与序列化之前相同。因此盲目地“恢复原始数组”基本上会导致对象以错误的哈希码存储在表中。

但在这里,相反的情况发生了:键(即 type 的对象Element)被反序列化。它们包含idFromElement地图,而地图又包含Element对象。这些元件被放入地图,所述Element对象是仍处于被反序列化的过程中,使用该putVal方法。但是由于 中的更改顺序ObjectInputStream,这是设置id字段的原始值(确定哈希码)之前完成的。因此,对象使用哈希码存储0,然后id分配值(例如值222),导致对象以它们实际上不再具有的哈希码结束在表中。


现在,在更抽象的层面上,这从观察到的行为中已经很清楚了。因此,最初的问题不是“这里发生了什么???”,而是

如果我提出的解决方法看起来没问题,或者我可以做一些更好的事情。

我认为解决方法可能没问题,但会犹豫地说那里不会出错。情况很复杂。

从第二部分开始:更好的方法是在Java Bug Database 中提交错误报告,因为新行为显然已被破坏。可能很难指出违反的规范,但反序列化的映射肯定不一致,这是不可接受的。


(是的,我也可以提交错误报告,但我认为可能需要进行更多研究以确保其编写正确,而不是重复,等等......)