Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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)
like image 502
Clément Hurlin Avatar asked Feb 02 '12 10:02

Clément Hurlin


2 Answers

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);
                }
            };
like image 96
Clément Hurlin Avatar answered Nov 15 '22 14:11

Clément Hurlin


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);
    }
like image 30
oae Avatar answered Nov 15 '22 14:11

oae