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

时间:2019-06-12 18:45:47

标签: java serialization java-8 deserialization 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;
    }
}

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

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

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

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

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

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

我想出的解决方案是使idFromElement过渡并编写自定义writeObjectreadObject方法来强制idFromElementid之后反序列化:

...
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();
}

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

  

对于可序列化的类,将设置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;
    }
}

1 个答案:

答案 0 :(得分:3)

正如评论中提到的那样,在问问者的鼓励下,以下是我假设在版本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;
}
...

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

desc.setPrimFieldValues(obj, primVals);

而且重要的是:这种情况发生在之前之前,它会为每个原始字段调用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);
}

...

已引入内部类FieldValuesdefaultReadFields方法现在仅读取字段值,并将其作为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);
        }
    }
}

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

Holger已经在注释中暗示了这样做的必要性:不能保证反序列化键的哈希码与序列化之前的相同。因此,盲目地“还原原始数组”基本上可能导致对象以错误的哈希码存储在表中。

但是在这里,情况恰恰相反:键(即Element类型的对象)被反序列化。它们包含idFromElement映射,而映射又包含Element对象。这些元素被放入地图中,而Element对象仍在使用putVal方法进行反序列化的过程中。但是由于ObjectInputStream中顺序的更改,此操作是在设置id字段的原始值(确定散列码)之前 完成的。因此,使用哈希码0存储对象,然后分配id值(例如值222),导致对象以哈希码结尾在表中他们实际上不再拥有了。


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

  

如果我建议的解决方法看起来不错,或者有更好的办法可以解决。

我认为解决方法可以可以,但是会毫不犹豫地说在那里没有任何问题。情况很复杂。

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


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