2012-02-02 5 views
9

Durante la deserializzazione di una delle nostre strutture dati (utilizzando il meccanismo predefinito (nessun oggetto writeObject/readObject personalizzato)), viene visualizzata un'istanza di ImmutableMap $ SerializedForm (dalla libreria Guava di google).readResolve non funziona? : viene visualizzata un'istanza di SerializedForm di Guava

Tale istanza non dovrebbe essere visibile dai client di guava perché le istanze di SerializedForm vengono sostituite utilizzando readResolve (si veda ad esempio "writeReplace" nella classe com.google.common.collect.ImmutableMap).

Quindi deserializzazione non riesce con il seguente messaggio:

java.lang.ClassCastException: cannot assign instance of com.google.common.collect.ImmutableMap$SerializedForm 
to field .. of type java.util.Map in instance of com.blah.C 

Questo è giusto in quanto ImmutableMap $ SerializedForm non è un sottotipo di java.util.Map, eppure avrebbe dovuto essere sostituito. Cosa sta andando storto?

Non abbiamo personalizzato writeObject/readObject nella classe com.blah.C. Abbiamo codice di serializzazione personalizzato negli oggetti parent (che contengono com.blah.C).

aggiornamento, ecco la parte superiore del stacktrace:

java.lang.ClassCastException: cannot assign instance of com.google.common.collect.ImmutableSet$SerializedForm to field com.blah.ast.Automaton.bodyNodes of type java.util.Set in instance of com.blah.ast.Automaton 
at java.io.ObjectStreamClass$FieldReflector.setObjFieldValues(ObjectStreamClass.java:2039) 
at java.io.ObjectStreamClass.setObjFieldValues(ObjectStreamClass.java:1212) 
at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:1952) 
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1870) 
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1752) 
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1328) 
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:350) 
at java.util.ArrayList.readObject(ArrayList.java:593) 
at sun.reflect.GeneratedMethodAccessor9.invoke(Unknown Source) 
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) 
at java.lang.reflect.Method.invoke(Method.java:597) 
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:974) 
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1848) 
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1752) 
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1328) 
at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:1946) 
at java.io.ObjectInputStream.defaultReadObject(ObjectInputStream.java:479) 
at com.blah.ast.AstNode.readObject(AstNode.java:189) 
at sun.reflect.GeneratedMethodAccessor10.invoke(Unknown Source) 
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) 
at java.lang.reflect.Method.invoke(Method.java:597) 
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:974) 
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1848) 
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1752) 
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1328) 
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:350) 
at java.util.ArrayList.readObject(ArrayList.java:593) 
at sun.reflect.GeneratedMethodAccessor9.invoke(Unknown Source) 
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) 
at java.lang.reflect.Method.invoke(Method.java:597) 
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:974) 
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1848) 
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1752) 
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1328) 
at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:1946) 
at java.io.ObjectInputStream.defaultReadObject(ObjectInputStream.java:479) 
at com.blah.ast.AstNode.readObject(AstNode.java:189) 
+0

si può mostrare la piena stacktrace? – axtavt

risposta

5

Questa settimana, abbiamo affrontato di nuovo questo bug; ma ho trovato la ragione di fondo. Il programma di caricamento classi utilizzato da ObjectInputStream dipende molto dal contesto (alcuni potrebbero dire indeterministico). Ecco la parte rilevante della documentazione di Sun (è un estratto da ObjectInputStream # resolveClass (ObjectStreamClass)):

[La classe loader] è determinato come segue: se c'è un metodo su stack del thread corrente la cui dichiarazione di classe è stato definito da un programma di caricamento classi definito dall'utente (e non è stato generato per implementare le chiamate riflessive), quindi è il programma di caricamento classe corrispondente al metodo più simile a quello attualmente in esecuzione; altrimenti, è nullo.Se questa chiamata risulta in ClassNotFoundException e il nome dell'istanza ObjectStreamClass passata è la parola chiave language Java per un tipo primitivo o vuoto, verrà restituito l'oggetto Class che rappresenta quel tipo primitivo o void (ad esempio, ObjectStreamClass con il nome "int "sarà risolto in Integer.TYPE). In caso contrario, ClassNotFoundException verrà lanciato al chiamante di questo metodo.

Nella nostra applicazione, abbiamo un plugin Eclipse B che dipende da un plugin A. utilità solo fossimo deserializzazione di oggetti le cui classi sono in B, ma deserializzazione è stato avviato in A (la creazione di un ObjectInputStream lì), e quello era il problema Raramente (cioè a seconda della pila di chiamate come dice il doc) la deserializzazione ha scelto il caricatore di classi sbagliato (uno che non poteva caricare classi B). Per risolvere questo problema, abbiamo passato un caricatore appropriato dal chiamante di deserializzazione di primo livello (in B) al metodo di utilità in A. Questo metodo ora utilizza un ObjectInputStream personalizzato come segue (notare la variabile gratuita "loader"):

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file)) { 
       @SuppressWarnings("rawtypes") 
       @Override 
       protected Class resolveClass(ObjectStreamClass objectStreamClass) 
         throws IOException, ClassNotFoundException { 
        return Class.forName(objectStreamClass.getName(), true, loader); 
       } 
      }; 
+0

L'ho visto anche negli ambienti OSGi. Avevo un bundle responsabile della serializzazione [de] nel livello database in cui non avevo il controllo su come venivano usati i programmi di caricamento di classe. Di conseguenza ho dovuto impostare DynamicImport-Package: * per assicurarmi che tutte le classi potessero essere viste.Sfortunatamente mi ero dimenticato di esportare un pacchetto da un pacchetto che conteneva classi da deserializzare, e inoltre ho dimenticato di implementare un proxy di serializzazione in questa classe. Ho eseguito un debugger e ho visto anche che si verificava ClassNotFoundException. Peccato che questo non è segnalato! –

3

Abbiamo trovato come evitare il bug, ma non abbiamo trovato ciò che l'ha causato.

Quando deserializziamo un'istanza di ArrayListMultiMap, il programma di caricamento classi non riesce a trovare uno della nostra classe (com.blah ....), perché viene utilizzato il programma di caricamento classi di Guava (nel codice chiamato da ObjectInputStream # resolveClass) invece del valore predefinito caricatore di classe. Quindi, ObjectInputStream propaga l'errore riempiendo l'istanza delle voci HandleList # con ClassCastExceptions. Tali eccezioni alla fine causano il salto di un readResolve, il che spiega il motivo per cui viene visualizzato un SerializedForm $ ImmutableMap.

Ciò che è strano è che serializziamo e deserializza molte altre strutture di dati (sia nostre sia di guava). Serializzare l'ArrayListMultimap di sé (con un oggetto writeObject personalizzato) evita il bug (anche se serializziamo le istanze delle raccolte di Guava (non quelle di Multimaps)).

Non capiamo perché il caricatore di classe diventi improvvisamente sbagliato, ma un bug deve essere in agguato da qualche parte. Credo che abbiamo ricevuto ClassCastException anziché ClassNotFoundException, perché la gestione degli errori in ObjectInputStream è errata (readResolve non dovrebbe essere saltato anche se mancasse qualche classe).

2

Il problema è che writeReplace()/readResolve() non funziona bene con i riferimenti circolari nel grafico dell'oggetto. writeReplace() e readResolve() sono asimmetrici. Durante la serializzazione, Java sostituirà tutti i riferimenti, inclusi i riferimenti circolari. Ma durante la deserializzazione, Java non risolverà i riferimenti circolari. Questo è sfortunatamente in base alla progettazione. Da the serialization spec:

Nota - Il metodo readResolve non viene richiamato sull'oggetto finché l'oggetto è completamente costruito, quindi tutti i riferimenti a questo oggetto nel suo grafico oggetto non verrà aggiornato al nuovo oggetto nominato dal readResolve. Tuttavia, durante la serializzazione di un oggetto con il metodo writeReplace , tutti i riferimenti all'oggetto originale nel grafico dell'oggetto dell'oggetto sostitutivo vengono sostituiti con i riferimenti all'oggetto di sostituzione . Pertanto, nei casi in cui un oggetto è serializzato , un oggetto sostitutivo il cui oggetto ha un riferimento all'oggetto originale, la deserializzazione comporterà un grafico degli oggetti non corretto . Inoltre, se i tipi di riferimento dell'oggetto letto (nominato da writeReplace) e l'oggetto originale non sono compatibili, la costruzione del grafico dell'oggetto genererà un valore di ClassCastException .

Gli sviluppatori Guava potrebbero aggirare questo problema rendendo ImmutableMap $ SerializedForm estendere ImmutableMap e delegare all'istanza ImmutableMap corretta. Quando si verifica un riferimento circolare, il chiamante otterrà SerializedForm invece di un riferimento diretto a ImmutableMap, ma è meglio di un ClassCastException.

0

Aveva lo stesso problema. Si è scoperto che la classe degli oggetti membri di una lista immutabile non si trovava sul percorso di classe del lato di deserializzazione. Ma questo fatto era nascosto dietro ClassCastException.

Ora sto facendo meglio il rilevamento degli errori con questo costrutto:

final ImmutableSet.Builder<Object> notFoundClasses = ImmutableSet.builder(); 
    try { 
     ObjectInputStream objectInputStream = new ObjectInputStream(inputStream) { 
      @Override 
      protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException { 
       try { 
        return super.resolveClass(desc); 
       } catch (ClassNotFoundException e) { 
        notFoundClasses.add(desc.getName()); 
        throw e; 
       } 
      } 
     }; 
     return (T) objectInputStream.readObject(); 
    } catch (ClassCastException e) { 
     throw Exceptions.runtime(e, "ClassCastException while de-serializing '%s', classes not found are: %s", objectClass, notFoundClasses.build()); 
    } catch (IOException | ClassNotFoundException e) { 
     throw Exceptions.runtime(e, "Could not de-serialize '%s'", objectClass); 
    }