readResolve 不起作用? :出现 Guava 的 SerializedForm 实例

发布于 2025-01-01 22:15:30 字数 3545 浏览 0 评论 0原文

在我们的数据结构之一的反序列化过程中(使用默认机制(无自定义 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

少跟Wǒ拽 2025-01-08 22:15:30

本周,我们再次遇到这个错误;但我找到了根本原因。 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);
                }
            };

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);
                }
            };
友谊不毕业 2025-01-08 22:15:30

请提交错误:
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!

秋凉 2025-01-08 22:15:30

我们找到了如何避免该错误,但没有找到导致该错误的原因。

当我们反序列化 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).

痕至 2025-01-08 22:15:30

问题是 writeReplace()/readResolve() 不能很好地处理对象图中的循环引用。 writeReplace() 和 readResolve() 是不对称的。在序列化期间,Java 将替换所有引用,包括循环引用。但在反序列化过程中,Java不会解析循环引用。不幸的是,这是设计使然。来自序列化规范

注意 - readResolve 方法不会在对象上调用,直到
对象已完全构造,因此在其对象中对此对象的任何引用
对象图不会更新为指定的新对象
阅读解决。然而,在对象的序列化过程中
writeReplace 方法中,对原始对象的所有引用
替换对象的对象图被替换为对
替换对象。因此,如果一个对象是
序列化指定一个替换对象,其对象图具有
对原始对象的引用,反序列化将导致
对象图不正确。此外,如果引用类型
正在读取的对象(由 writeReplace 指定)和原始对象
不兼容,对象图的构造会引发
类转换异常。

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:

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.

ゃ懵逼小萝莉 2025-01-08 22:15:30

有同样的问题。结果发现,不可变列表的成员对象的类不在反序列化端的类路径上。
但这个事实隐藏在 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);
    }

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);
    }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文