readResolve 不起作用? :出现 Guava 的 SerializedForm 实例
在我们的数据结构之一的反序列化过程中(使用默认机制(无自定义 writeObject/readObject)),会显示一个 ImmutableMap$SerializedForm 实例(来自 google 的 Guava 库)。
这样的实例对于 guava 客户端来说不应该是可见的,因为 SerializedForm 的实例是使用 readResolve 替换的(例如,参见 com.google.common.collect.ImmutableMap 类中的“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)
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)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
本周,我们再次遇到这个错误;但我找到了根本原因。 ObjectInputStream 使用的类加载器高度依赖于上下文(有些人会说不确定)。以下是 Sun 文档的相关部分(摘自 ObjectInputStream#resolveClass(ObjectStreamClass)):
在我们的应用程序中,我们有一个 Eclipse 插件 B,它依赖于一个纯实用程序插件 A。我们正在反序列化类位于 B 中的对象,但反序列化是在 A 中启动的(在那里创建一个 ObjectInputStream),这就是问题所在。很少(即取决于文档所说的调用堆栈)反序列化选择了错误的类加载器(无法加载 B 类)。为了解决这个问题,我们将一个适当的加载器从顶级反序列化调用程序(B 中)传递到 A 中的实用程序方法。该方法现在使用自定义 ObjectInputStream,如下所示(请注意自由变量“loader”):
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)):
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"):
请提交错误:
http://code.google.com/p/guava-libraries/issues/entry
如果您可以附加一个为您触发此错误的独立程序,那将会有所帮助!
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!
我们找到了如何避免该错误,但没有找到导致该错误的原因。
当我们反序列化 ArrayListMultiMap 的实例时,类加载器找不到我们的类之一(com.blah....),因为使用了 Guava 的类加载器(在从 ObjectInputStream#resolveClass 调用的代码中)而不是默认的类加载器。然后,ObjectInputStream 通过使用 ClassCastExceptions 填充其 HandleList#entries 实例来传播失败。此类异常最终会导致 readResolve 被跳过,这解释了为什么会出现 ImmutableMap$SerializedForm。
奇怪的是我们序列化和反序列化许多其他数据结构(我们自己的和番石榴的)。我们自己序列化 guava 的 ArrayListMultimap(使用自定义 writeObject)可以避免该错误(即使我们序列化 Guava 集合的实例(尽管不是 Multimap))。
我们不明白为什么类加载器突然出错,但一定是某个地方潜伏着一个错误。我相信我们收到了 ClassCastException 而不是 ClassNotFoundException,因为 ObjectInputStream 中的错误处理是错误的(即使缺少某些类,也不应该跳过 readResolve)。
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).
问题是 writeReplace()/readResolve() 不能很好地处理对象图中的循环引用。 writeReplace() 和 readResolve() 是不对称的。在序列化期间,Java 将替换所有引用,包括循环引用。但在反序列化过程中,Java不会解析循环引用。不幸的是,这是设计使然。来自序列化规范:
Guava 开发人员可以通过使 ImmutableMap$SerializedForm 扩展 ImmutableMap 并委托给适当的 ImmutableMap 实例来解决这个问题。当发生循环引用时,调用者将获得 SerializedForm 而不是直接引用 ImmutableMap,但它比 ClassCastException 更好。
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:
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.
有同样的问题。结果发现,不可变列表的成员对象的类不在反序列化端的类路径上。
但这个事实隐藏在 ClassCastException 后面。
现在我正在使用此构造进行更好的错误检测:
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: