comportamento desserialização diferente entre Java 8 e Java 11

Anderson Vieira :

Eu tenho um problema com a desserialização em Java 11 que resulta em um HashMapcom uma chave que não pode ser encontrado. Eu apreciaria se alguém com mais conhecimento sobre o assunto poderia dizer se a minha solução proposta parece ok, ou se há algo melhor que eu podia fazer.

Considere a seguinte implementação artificial (as relações em que o verdadeiro problema é um pouco mais complexa e difícil de mudança):

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

Então eu criar uma instância que tem uma referência a si mesmo e serialize e desserializá-lo:

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

Se eu executar esse código em Java 8 o resultado é "ok", se eu executá-lo em Java 11 O resultado é uma NullPointerExceptionporque retrievedElement.idFrom(retrievedElement)retornos null.

Eu coloquei um ponto de parada no HashMap.hash()e notou que:

  • Em Java 8, quando idFromElementestá sendo desserializado e Element(222)está sendo adicionado a ele, a sua idé 222, então eu sou capaz de encontrá-lo mais tarde.
  • Em Java 11, o idnão é inicializado (0 para intou nulo, se eu torná-lo um Integer), então hash()é 0 quando ele é armazenado no HashMap. Mais tarde, quando eu tentar recuperá-la, o idé 222, então idFromElement.get(element)retorna null.

Eu entendo que a sequência aqui é deserialize (elemento (222)) -> deserialize (idFromElement) -> colocar Elemento inacabada (222) em Mapa. Mas, por alguma razão, em Java 8 idjá está inicializado quando chegar ao último passo, enquanto em Java 11 não é.

A solução que eu vim com era fazer idFromElementtransitório e gravação personalizada writeObjecte readObjectmétodos para forçar idFromElementa ser desserializado depois 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();
}

A única referência que eu era capaz de encontrar sobre a ordem durante a serialização / desserialização foi o seguinte:

Para as classes serializáveis, a bandeira SC_SERIALIZABLE estiver definido, o número de campos de conta o número de campos serializados e é seguido por um descritor para cada campo serializado. Os descritores são escritos em ordem canônica. Os descritores para campos digitados primitivos são escritos primeiro classificado por nome de campo seguido de descritores para o objeto digitado campos ordenados pelo nome do campo. Os nomes são classificados usando String.compareTo.

Que é o mesmo em ambos Java 8 e Java 11 docs, e parece implicar que os campos digitados primitivas devem ser escritos em primeiro lugar, então eu esperava que haveria nenhuma diferença.


Implementação de Storage<T>incluídos para conclusão:

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

Como mencionado nos comentários e encorajado pelo autor da questão, aqui estão as partes do código que mudou entre a versão 8 e versão 11 que eu supor para ser a razão para o comportamento diferente (com base na leitura e depuração).

A diferença está na ObjectInputStreamclasse, em um dos seus métodos de núcleo. Esta é a parte relevante da implementação em 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;
}
...

O método chama defaultReadFields, que lê os valores dos campos. Como mencionado na parte citado da especificação, ele primeiro lida com os descritores de campo de primitivas campos. Os valores que são lidos para esses campos são definidos imediatamente após lê-los , com

desc.setPrimFieldValues(obj, primVals);

e mais importante: isso acontece antes que chama readObject0para cada um dos não campos -primitive.

Em contraste com isso, aqui está a parte relevante da implementação do 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);
}

...

Uma classe interna, FieldValues, foi introduzido. O defaultReadFieldsmétodo agora só os valores do campo, e retorna-los como um FieldValuesobjeto. Depois, os valores devolvidos são atribuídos aos campos, passando este FieldValuesobjeto para um recém-introduzido defaultSetFieldValuesmétodo, que faz internamente a desc.setPrimFieldValues(obj, primValues)chamada que originalmente foi feito imediatamente após os valores primitivos tinham sido ler.

Para enfatizar isso novamente: O defaultReadFieldsmétodo primeiro os valores dos campos primitivos. Em seguida, ele os valores do campo não-primitivos. Mas ele faz isso antes de os valores dos campos primitivos foram criados!

Este novo processo interfere com o método de deserialização HashMap. Novamente, a parte relevante é mostrado aqui:

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

Ele lê os objetos KEY- e valor, um por um, e coloca-los na tabela, calculando o hash da chave e usando o interno putValmétodo. Este é o mesmo método que é utilizado quando preencher manualmente o mapa (isto é, quando ela está cheia de programação, e não desserializadas).

Holger já deu uma dica nos comentários por que isso é necessário: Não há garantia de que o código de hash das chaves desserializados será o mesmo que antes da serialização. Tão cegamente "restaurar a matriz original" poderia basicamente levar a objetos que estão sendo armazenados na tabela sob um código de hash errado.

Mas aqui, acontece o contrário: as teclas (ou seja, os objetos do tipo Element) são desserializado. Eles contêm o idFromElementmapa, que por sua vez contém os Elementobjetos. Estes elementos são colocados no mapa, enquanto que os Elementobjectos são ainda, no processo de ser desserializadas, usando o putValmétodo. Mas, devido à ordem alterada em ObjectInputStream, isso é feito antes de o valor primitivo do idcampo (que determina o código de hash) foi definido. Assim, os objectos são armazenados usando o código de hash 0e, mais tarde, os idvalores é atribuído (por exemplo, o valor 222), fazendo com que os objectos a acabar na tabela sob um código de hash que realmente não tem mais.


Agora, em um nível mais abstrato, isso já estava claro desde o comportamento observado. Portanto, a pergunta original não "O que está acontecendo aqui ???", mas

se a minha solução proposta parece ok, ou se há algo melhor que eu podia fazer.

Eu acho que a solução poderia ser OK, mas hesitaria em dizer que nada poderia dar errado lá. É complicado.

A partir da segunda parte: Algo melhor poderia ser a de enviar um relatório de bug no Bug banco de dados Java , porque o novo comportamento é claramente quebrado. Pode ser difícil apontar uma especificação que é violada, mas o mapa desserializado é certamente inconsistente , e isso não é aceitável.


(Sim, eu também poderia apresentar um relatório de bug, mas acho que mais pesquisas podem ser necessárias, a fim de se certificar de que está escrito corretamente, não uma duplicata, etc ....)

Acho que você gosta

Origin http://43.154.161.224:23101/article/api/json?id=227324&siteId=1
Recomendado
Clasificación