readResolve not working ? : an instance of Guava's SerializedForm appears
Asked Answered
F

5

12

During deserialization of one of our data structure (using the default mechanism (no custom writeObject/readObject)), an instance of ImmutableMap$SerializedForm (from google's Guava library) shows up.

Such an instance should not be visible from clients of guava because instances of SerializedForm are replaced using readResolve (see for example "writeReplace" in class com.google.common.collect.ImmutableMap).

Hence deserialization fails with the following message :

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

This is right since ImmutableMap$SerializedForm is not a subtype of java.util.Map, yet it should have been replaced. What is going wrong ?

We have no custom writeObject/readObject in class com.blah.C. We do have custom serialization code in parent objects (that contain com.blah.C).

update, here's the top of the 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)
Fireweed answered 2/2, 2012 at 10:25 Comment(1)
Can you show the full stacktrace?Elfredaelfrida
F
8

This week, we faced again this bug; but I found the root reason. The class loader used by ObjectInputStream is highly-context dependant (some would say indeterministic). Here is the relevant part of Sun's documentation (it's an excerpt from ObjectInputStream#resolveClass(ObjectStreamClass)):

[The class loader] is determined as follows: if there is a method on the current thread's stack whose declaring class was defined by a user-defined class loader (and was not a generated to implement reflective invocations), then it is the class loader corresponding to the closest such method to the currently executing frame; otherwise, it is null. If this call results in a ClassNotFoundException and the name of the passed ObjectStreamClass instance is the Java language keyword for a primitive type or void, then the Class object representing that primitive type or void will be returned (e.g., an ObjectStreamClass with the name "int" will be resolved to Integer.TYPE). Otherwise, the ClassNotFoundException will be thrown to the caller of this method.

In our application, we have an Eclipse plugin B that depends on an utility-only plugin A. We were deserializing objects whose classes are in B, but deserialization was initiated in A (creating an ObjectInputStream there), and that was the problem. Rarely (i.e. depending on the call stack as the doc says) deserialization chose the wrong class loader (one that could not load B-classses). To solve this problem, we passed an appropriate loader from the top-level deserialization caller (in B) to the utility method in A. This method now uses a custom ObjectInputStream as follows (note the free variable "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);
                }
            };
Fireweed answered 22/2, 2012 at 8:16 Comment(1)
I've also seen this in OSGi environments. I had one bundle responsible for [de]serialisation at the database layer where I had no control over how class loaders were used. As a result I had to set DynamicImport-Package: * to make sure all classes could be seen. Unfortunately I'd forgotten to export a package from a bundle which contained classes to deserialize, and furthermore forgot to implement a serialisation proxy in this class. I ran a debugger and I too saw the ClassNotFoundException occurring. Shame this isn't reported!Freda
E
3

Please file a bug: http://code.google.com/p/guava-libraries/issues/entry

If you can attach a standalone program that triggers this error for you, that would help!

Entire answered 2/2, 2012 at 15:9 Comment(1)
I could not reproduce the bug on a small standalone program.Outdoor
F
3

We found how to avoid the bug, but did not find what caused it.

When we deserialize an instance of ArrayListMultiMap, the class loader cannot find one of our class (com.blah....), because Guava's class loader is used (in code called from ObjectInputStream#resolveClass) instead of the default class loader. Then, ObjectInputStream propagates the failure by filling its instance of HandleList#entries with ClassCastExceptions. Such exceptions ultimately cause a readResolve to be skipped, which explains why an ImmutableMap$SerializedForm shows up.

What is weird is that we serialize and deserialize a lot of other data structures (both our own and guava's). Serializing guava's ArrayListMultimap ourself (with a custom writeObject) avoids the bug (even if we serialize instances of Guava's collections (not Multimaps though)).

We do not understand why the class loader suddenly becomes wrong, but a bug must be lurking somewhere. I believe we received ClassCastException instead of ClassNotFoundException, because error handling in ObjectInputStream is wrong (readResolve should not have been skipped even if some class is missing).

Fireweed answered 3/2, 2012 at 8:31 Comment(0)
A
2

The problem is that writeReplace()/readResolve() don't play nicely with circular references in your object graph. writeReplace() and readResolve() are asymmetric. During serialization, Java will replace all references, including circular references. But during deserialization, Java will not resolve circular references. This is unfortunately by design. From the serialization spec:

Note - The readResolve method is not invoked on the object until the object is fully constructed, so any references to this object in its object graph will not be updated to the new object nominated by readResolve. However, during the serialization of an object with the writeReplace method, all references to the original object in the replacement object's object graph are replaced with references to the replacement object. Therefore in cases where an object being serialized nominates a replacement object whose object graph has a reference to the original object, deserialization will result in an incorrect graph of objects. Furthermore, if the reference types of the object being read (nominated by writeReplace) and the original object are not compatible, the construction of the object graph will raise a ClassCastException.

The Guava developers could work around this problem by making ImmutableMap$SerializedForm extend ImmutableMap and delegate to the proper ImmutableMap instance. When a circular reference occurs, the caller will get the SerializedForm instead of a direct reference to the ImmutableMap, but it's better than a ClassCastException.

Anaanabaena answered 6/9, 2013 at 0:17 Comment(0)
W
1

Had the same problem. Turned out that the class of the member objects of an immutable list were not on the classpath of the deserialization side. But that fact was hidden behind the ClassCastException.

Now i'm doing better error detection with this construct:

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);
    }
Whinstone answered 30/3, 2016 at 12:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.