在我们的一个数据结构的反序列化过程中(使用默认机制(没有自定义的writeObject/readObject)),会显示ImmutableMap$SerializedForm的实例(来自谷歌的Guava库)。
这样的实例在番石榴的客户端上不应该可见,因为SerializedForm的实例是使用readResolve替换的(例如,请参阅com.google.common.collect.ImutableMap类中的"writeReplace")
因此,反序列化失败,并显示以下消息:
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
这是正确的,因为ImmutableMap$SerializedForm还不是java.util.Map的子类型它应该被替换掉。出了什么问题?
我们在类com.blah.C中没有自定义的writeObject/readObject。我们在父对象(包含com.blah.C)中有自定义的序列化代码。
更新,这里是堆栈的顶部:
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)
本周,我们再次面临此错误;但我找到了根本原因。ObjectInputStream使用的类加载器高度依赖于上下文(有些人会说不确定)。以下是Sun文档的相关部分(摘录自ObjectInputStream#resolveClass(ObjectStreamClass)):
【类加载器】的确定如下:如果当前线程堆栈上有一个方法的声明类是由用户定义的类加载器定义的(并且不是为实现反射调用而生成的),那么它就是与当前执行帧最接近的此类方法对应的类加载器;否则为空。如果此调用导致ClassNotFoundException,并且传递的ObjectStreamClass实例的名称是基元类型或void的Java语言关键字,则将返回表示该基元类型和void的Class对象(例如,名称为"int"的ObjectStreamClass将解析为Integer.type)。否则,ClassNotFoundException将被抛出到此方法的调用方。
在我们的应用程序中,我们有一个Eclipse插件B,它依赖于一个仅限实用程序的插件A。我们正在反序列化类在B中的对象,但反序列化是在A中启动的(在那里创建一个ObjectInputStream),这就是问题所在。反序列化很少选择错误的类加载器(无法加载B类的类加载器)(如文档所述,这取决于调用堆栈)。为了解决这个问题,我们从顶级反序列化调用程序(B中)向A中的实用程序方法传递了一个适当的加载器。该方法现在使用自定义ObjectInputStream,如下所示(注意自由变量"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);
}
};
请提交一个错误:http://code.google.com/p/guava-libraries/issues/entry
如果你能附加一个独立的程序来触发这个错误,那会有帮助!
我们找到了如何避免错误,但没有找到导致错误的原因。
当我们反序列化ArrayListMultiMap的实例时,类加载器找不到我们的一个类(com.blah….),因为使用了Guava的类加载器(在从ObjectInputStream#resolveClass调用的代码中)而不是默认的类加载器。然后,ObjectInputStream通过用ClassCastExceptions填充其HandleList#条目实例来传播失败。这样的异常最终会导致跳过readResolve,这就解释了为什么会出现ImmutableMap$SerializedForm。
奇怪的是,我们序列化和反序列化了许多其他数据结构(包括我们自己的和番石榴的)。序列化guava的ArrayListMultimap-urself(使用自定义writeObject)可以避免错误(即使我们序列化guava集合的实例(但不是Multimaps))。
我们不明白为什么类加载程序突然出错,但一定有一个bug潜伏在某个地方。我相信我们收到的是ClassCastException而不是ClassNotFoundException,因为ObjectInputStream中的错误处理是错误的(即使缺少某些类,也不应该跳过readResolve)。
问题是writeReplace()/readResolve()不能很好地处理对象图中的循环引用。writeReplace()和readResolve()是不对称的。在序列化期间,Java将替换所有引用,包括循环引用。但在反序列化过程中,Java不会解析循环引用。不幸的是,这是故意的。来自序列化规范:
注意-直到对象是完全构造的,因此它的对象图将不会更新为指定的新对象readResolve。但是,在使用writeReplace方法,中对原始对象的所有引用替换对象的对象图被替换为对替换对象。因此,在对象序列化指定替换对象,该替换对象的对象图具有引用原始对象时,反序列化将导致对象的图形不正确。此外,如果正在读取的对象(由writeReplace指定)和原始对象不兼容,对象图的构造将引发ClassCastException。
Guava开发人员可以通过使ImmutableMap$SerializedForm扩展ImmutableMap并委托给适当的ImmutableMap实例来解决这个问题。当发生循环引用时,调用方将获得SerializedForm,而不是对ImmutableMap的直接引用,但这比ClassCastException要好。也有同样的问题。事实证明,不可变列表的成员对象的类不在反序列化端的类路径上。但这一事实隐藏在ClassCastException的背后。
现在我用这个结构做了更好的错误检测:
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);
}