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

标签 java serialization java-8 deserialization java-11

我在 Java 11 中遇到反序列化问题,导致 HashMap用找不到的 key 。如果对这个问题有更多了解的人可以说我提出的解决方法是否可行,或者我是否可以做一些更好的事情,我将不胜感激。

考虑以下人为的实现(实际问题中的关系有点复杂且难以改变):

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 中运行此代码,结果是“ok”,如果我在 Java 11 中运行它,结果是 NullPointerException因为 retrievedElement.idFrom(retrievedElement)返回 null .

我在 HashMap.hash() 处设置了一个断点并注意到:
  • 在 Java 8 中,当 idFromElement正在反序列化和 Element(222)正在添加到它,它的 id是 222,所以我以后可以找到它。
  • 在 Java 11 中,id未初始化(0 表示 int 或 null 如果我将其设为 Integer ),所以 hash()当它存储在 HashMap 中时为 0 .后来,当我尝试检索它时,id是 222,所以 idFromElement.get(element)返回 null .

  • 我理解这里的顺序是 deserialize(Element(222)) -> deserialize(idFromElement) -> put unfinished Element(222) into Map。但是,出于某种原因,在 Java 8 中 id当我们到达最后一步时已经初始化,而在 Java 11 中它没有。

    我想出的解决方案是制作 idFromElement transient 和写自定义 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();
    }
    

    我在序列化/反序列化过程中能够找到的关于订单的唯一引用是:

    For serializable classes, the SC_SERIALIZABLE flag is set, the number of fields counts the number of serializable fields and is followed by a descriptor for each serializable field. The descriptors are written in canonical order. The descriptors for primitive typed fields are written first sorted by field name followed by descriptors for the object typed fields sorted by field name. The names are sorted using String.compareTo.



    两者Java 8相同和 Java 11 docs,似乎暗示应该首先编写原始类型的字段,所以我预计不会有什么区别。
    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;
        }
    }
    

    最佳答案

    正如评论中提到的并受到提问者的鼓励,以下是在版本 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 ,它读取字段的值。正如规范的引用部分所述,它首先处理原始字段的字段描述符。为这些字段读取的值 阅读后立即设置 , 与
    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);
    }
    
    ...
    

    一个内部类,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);
            }
        }
    }
    

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

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

    但在这里,情况正好相反:键(即 Element 类型的对象)被反序列化。它们包含 idFromElement map ,其中又包含 Element对象。这些元素被放入 map 中,而Element对象仍在反序列化过程中,使用 putVal方法。但是由于ObjectInputStream中的顺序改变了,这是在 id 的原始值之前完成的字段(确定哈希码)已设置。所以对象是使用哈希码存储的 0 ,以及后来的 id值被分配(例如值 222 ),导致对象以它们实际上不再具有的哈希码结束在表中。

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

    if my proposed workaround looks ok, or if there is something better I could do.



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

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

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

    关于java - Java 8 和 Java 11 之间的不同反序列化行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56568575/

    相关文章:

    java - Reduce 为并行流返回不可预测的结果

    java - 使用 Comparator 对列表进行排序时出现 java.lang.illegalArgumentException : comparison method violates its general contract !

    java - 使用 Java 可选类表示内部 if else block

    java - 如何对无限循环 java nio watchservice 程序进行基准测试

    java - 反序列化定义已更改的类

    java - Hibernate:空集合而不是延迟加载错误

    wpf - WPF 中数据绑定(bind) ObservableCollection 的序列化(PropertyChangedEventManager)

    java - 将 post 参数映射到请求中的 DTO

    java - 使用 Jersey 配置 Swagger-ui

    java - 连续调用 'SOAPMessage.writeTo' 抛出读取错误