ClassCastException: cannot assign SerializedLambda to field with ClassLoader
Peter Levart
peter.levart at gmail.com
Tue Mar 26 08:06:36 UTC 2019
As I have suspected, this code works (see that there's no custom class
loaders involved as they are unimportant in this situation):
package test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class SerializedLambdaTest implements Runnable {
public void run() { new MyCode().run(); }
public interface SerializableRunnable extends Runnable, Serializable {
}
public static class MyCode implements SerializableRunnable {
Object runnable2 = (SerializableRunnable)
() -> System.out.println("HELLO" + this.getClass());
private byte[] serialize(Object o) {
ByteArrayOutputStream baos;
try (
ObjectOutputStream oos
= new ObjectOutputStream(baos = new
ByteArrayOutputStream())) {
oos.writeObject(o);
} catch (IOException e) {
throw new RuntimeException(e);
}
return baos.toByteArray();
}
private <T> T deserialize(byte[] bytes) {
try (
ObjectInputStream ois
= new ObjectInputStream(new
ByteArrayInputStream(bytes))) {
return (T) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
@Override
public void run() {
System.out.println(" this: " + this);
SerializableRunnable deSerializedThis
= deserialize(serialize(this));
System.out.println(" deSerializedThis: "
+ deSerializedThis);
SerializableRunnable runnable = (SerializableRunnable)
runnable2;
System.out.println(" runnable: " + runnable);
SerializableRunnable deSerializedRunnable
= deserialize(serialize(runnable));
System.out.println("deSerializedRunnable: "
+ deSerializedRunnable);
}
}
public static void main(String[] args) {
new SerializedLambdaTest().run();
}
}
...the original code failed with a very descriptive message:
Exception in thread "main" java.lang.ClassCastException: cannot assign
instance of java.lang.invoke.SerializedLambda to field
test.SerializedLambdaTest$MyCode.runnable2 of type
test.SerializedLambdaTest$SerializableRunnable in instance of
test.SerializedLambdaTest$MyCode
at
java.base/java.io.ObjectStreamClass$FieldReflector.setObjFieldValues(ObjectStreamClass.java:2190)
at
java.base/java.io.ObjectStreamClass$FieldReflector.checkObjectFieldValueTypes(ObjectStreamClass.java:2153)
at
java.base/java.io.ObjectStreamClass.checkObjFieldValueTypes(ObjectStreamClass.java:1407)
at
java.base/java.io.ObjectInputStream.defaultCheckFieldValues(ObjectInputStream.java:2371)
at
java.base/java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:2278)
...
which suggests that my claims are correct...
Peter
On 3/26/19 8:54 AM, Peter Levart wrote:
> Hi Seth,
>
> I think you stumbled on a "defect" of Java Serialization mechanism.
> Namely, the inability for it to correctly deserialize object graphs
> containing a cycle when an object in such cycle uses the
> "readResolve()" special method to replace the deserialized object with
> a replacement object. SerializedLambda is a class with instances that
> are 1st deserialized from stream then "replaced" with real lambda
> objects via the "readResolve()" method which constructs and returns
> the "real" lambda object. The deserialized SerializedLambda object is
> nevertheless attempted to be (temporarily) assigned to the field that
> would hold the final lambda object, which fails as the filed is of
> different type than SerializedLambda (or supertype).
>
> You can check my claims by changing the type of the field holding the
> lambda to java.lang.Object and see if it works... (It should if my
> memory serves me well :-)
>
> This is a known defect of Java serialization and is not specific to
> serialized lambda.
>
> There were some unsuccessful attempts to fix it during (re)discovery
> when lambdas were being added to Java.
>
> Regards, Peter
>
> On 3/26/19 5:03 AM, seth lytle wrote:
>> it's a nested class, but it's loaded with the same class loader as the
>> toplevel.
>> which is what i thought you thought was the problem when
>>
>> you wrote:
>>> this test loads a nested type into a different classloader to its
>> enclosing type
>>
>> and:
>>> it is a requirement that all nest members be defined in the same
>>> package - which implies the same classloader
>>
>>
>>
>> On Mon, Mar 25, 2019 at 11:40 PM David Holmes <david.holmes at oracle.com>
>> wrote:
>>
>>> On 26/03/2019 1:06 pm, seth lytle wrote:
>>>> i still get the same stack trace after breaking the example out
>>>> into two
>>>> files. does that sufficiently address the nesting issue ?
>>> ??
>>>
>>> public static class MyCode ...
>>>
>>> is still a nested class.
>>>
>>> David
>>>
>>>> file: SerializedLambdaTest
>>>> package test;
>>>>
>>>> import java.io.ByteArrayInputStream;
>>>> import java.io.ByteArrayOutputStream;
>>>> import java.io.IOException;
>>>> import java.io.ObjectInputStream;
>>>> import java.io.ObjectOutputStream;
>>>> import java.io.Serializable;
>>>>
>>>> // from: https://bugs.openjdk.java.net/browse/JDK-8008770
>>>> public class SerializedLambdaTest implements Runnable {
>>>> public void run() { new MyCode().run(); }
>>>> public interface SerializableRunnable extends
>>>> Runnable,Serializable
>>> {
>>>> }
>>>> public static class MyCode implements SerializableRunnable {
>>>> SerializableRunnable runnable2 = ()
>>>> -> {
>>>> System.out.println("HELLO"+this.getClass());
>>>> };
>>>> private byte[] serialize(Object o) {
>>>> ByteArrayOutputStream baos;
>>>> try (
>>>> ObjectOutputStream oos
>>>> = new ObjectOutputStream(baos = new
>>>> ByteArrayOutputStream())) {
>>>> oos.writeObject(o);
>>>> } catch (IOException e) {
>>>> throw new RuntimeException(e);
>>>> }
>>>> return baos.toByteArray();
>>>> }
>>>> private <T> T deserialize(byte[] bytes) {
>>>> try (
>>>> ObjectInputStream ois
>>>> = new ObjectInputStream(new
>>>> ByteArrayInputStream(bytes))) {
>>>> return (T) ois.readObject();
>>>> } catch (IOException|ClassNotFoundException e) {
>>>> throw new RuntimeException(e);
>>>> }
>>>> }
>>>> @Override
>>>> public void run() {
>>>> System.out.println(" this: "+this);
>>>> SerializableRunnable deSerializedThis
>>>> = deserialize(serialize(this));
>>>> System.out.println(" deSerializedThis: "
>>>> +deSerializedThis);
>>>>
>>>> SerializableRunnable runnable = runnable2;
>>>> System.out.println(" runnable: "+runnable);
>>>> SerializableRunnable deSerializedRunnable
>>>> = deserialize(serialize(runnable));
>>>> System.out.println("deSerializedRunnable: "
>>>> +deSerializedRunnable);
>>>> }
>>>> }
>>>> }
>>>>
>>>>
>>>> file: MyClassLoader
>>>> package test;
>>>>
>>>> import java.io.IOException;
>>>> import java.io.InputStream;
>>>>
>>>> class MyClassLoader extends ClassLoader {
>>>> MyClassLoader(ClassLoader parent) {
>>>> super(parent);
>>>> }
>>>> @Override
>>>> protected Class<?> loadClass(String name,boolean resolve) throws
>>>> ClassNotFoundException {
>>>> if (name.startsWith("test."))
>>>> synchronized (getClassLoadingLock(name)) {
>>>> Class<?> c = findLoadedClass(name);
>>>> if (c==null) c = findClass(name);
>>>> if (resolve) resolveClass(c);
>>>> return c;
>>>> }
>>>> else return super.loadClass(name,resolve);
>>>> }
>>>> @Override
>>>> protected Class<?> findClass(String name) throws
>>>> ClassNotFoundException {
>>>> String path = name.replace('.','/').concat(".class");
>>>> try (final InputStream is = getResourceAsStream(path)) {
>>>> if (is!=null) {
>>>> byte[] bytes = is.readAllBytes();
>>>> return defineClass(name,bytes,0,bytes.length);
>>>> }
>>>> else throw new ClassNotFoundException(name);
>>>> } catch (IOException e) {
>>>> throw new ClassNotFoundException(name,e);
>>>> }
>>>> }
>>>> public static void main(String[] args) throws Exception {
>>>> ClassLoader myCl = new MyClassLoader(
>>>> MyClassLoader.class.getClassLoader()
>>>> );
>>>> Class<?> myCodeClass = Class.forName(
>>>> "test.SerializedLambdaTest",
>>>> true,
>>>> myCl
>>>> );
>>>> Runnable myCode = (Runnable) myCodeClass.newInstance();
>>>> myCode.run();
>>>> }
>>>> }
>>>>
>>>>
>>>>
>>>>
>>>> On Mon, Mar 25, 2019 at 10:34 PM David Holmes <david.holmes at oracle.com
>>>> <mailto:david.holmes at oracle.com>> wrote:
>>>>
>>>> Hi Seth,
>>>>
>>>> On 26/03/2019 12:16 pm, seth lytle wrote:
>>>> > i haven't changed the assignment from the original test case
>>>> (which was
>>>> > accepted as valid at the time). i haven't gone through it in
>>>> depth, but
>>>> > assume that it's ok and used it since people are already
>>>> familiar
>>>> with
>>>> > it. it appears to me that that example uses reflection to
>>>> allocate an
>>>> > instance using a definitive class loader and then uses a
>>>> runnable
>>>> for
>>>> > the serialization/deserialization cycle
>>>> >
>>>> > the class cast exception happens not at the example level, but
>>> deep
>>>> > inside `readObject` when it's doing an internal assignment
>>>> >
>>>> > perhaps my choice of words "into a new classloader" was
>>>> insufficient or
>>>> > misleading. in both examples that i've come up with so far,
>>>> the
>>>> class
>>>> > loader calls defineClass directly without first delegating to
>>>> super (for
>>>> > a subset of the classes). so "into a definitive classloader"
>>>> might have
>>>> > been a better choice
>>>>
>>>> I think the basic problem is that this test loads a nested
>>>> type into
>>> a
>>>> different classloader to its enclosing type. I can easily imagine
>>> this
>>>> messing up the code that gets generated to implement the lambda
>>>> expression - but I'd need to examine that code in detail to see
>>> exactly
>>>> why (others may know this more readily than I do). It's
>>>> unclear to me
>>>> whether this would be considered a bug or a "don't do that"
>>> situation.
>>>> As of JDK 11, nested types define "nests" at the VM level (see
>>> JEP-181)
>>>> and it is a requirement that all nest members be defined in
>>>> the same
>>>> package - which implies the same classloader.
>>>>
>>>> David
>>>>
>>>> >
>>>> >
>>>> >
>>>> >
>>>> >
>>>> > On Mon, Mar 25, 2019 at 9:34 PM David Holmes
>>>> <david.holmes at oracle.com <mailto:david.holmes at oracle.com>
>>>> > <mailto:david.holmes at oracle.com
>>>> <mailto:david.holmes at oracle.com>>> wrote:
>>>> >
>>>> > Hi Seth,
>>>> >
>>>> > On 26/03/2019 11:22 am, seth lytle wrote:
>>>> > > if a lambda is a field and captures `this`, and it's
>>>> deserialized
>>>> > into a
>>>> > > new class loader, it throws a ClassCastException.
>>>> >
>>>> > Not sure I follow. If you load a class into a different
>>>> classloader
>>>> > then
>>>> > you get a different type. It might appear the same to
>>>> you but
>>>> it is a
>>>> > distinct type, so you can't assign across different
>>>> instances
>>>> loaded by
>>>> > different classloaders.
>>>> >
>>>> > Cheers,
>>>> > David
>>>> > -----
>>>> >
>>>> > > i came across this bug
>>>> > > independently while writing a test, but then found
>>>> an old
>>>> openjdk
>>>> > bug with
>>>> > > a similar issue and tweaked the test case (source
>>>> below).
>>>> my version
>>>> > > differs only in
>>>> > > 1. moved the lambda to a field
>>>> > > 2. reference `this` in it
>>>> > > 3. uses readAllBytes instead of the internal method
>>>> > > 4. formatting
>>>> > >
>>>> > > running with java 11 or 12 (or java 8 using a
>>>> substitute
>>> for
>>>> > readAllBytes)
>>>> > > results in:
>>>> > > this:
>>>> test.SerializedLambdaTest$MyCode at 8efb846
>>>> > > deSerializedThis:
>>>> test.SerializedLambdaTest$MyCode at 2b71fc7e
>>>> > > runnable:
>>>> > >
>>>> >
>>>>
>>> test.SerializedLambdaTest$MyCode$$Lambda$1/0x0000000801188440 at 37bba400
>>>> > > Exception in thread "main"
>>>> java.lang.ClassCastException:
>>>> cannot
>>>> > assign
>>>> > > instance of java.lang.invoke.SerializedLambda to field
>>>> > > test.SerializedLambdaTest$MyCode.runnable2 of type
>>>> > > test.SerializedLambdaTest$SerializableRunnable in
>>>> instance
>>> of
>>>> > > test.SerializedLambdaTest$MyCode
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectStreamClass$FieldReflector.setObjFieldValues(ObjectStreamClass.java:2190)
>>>>
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectStreamClass$FieldReflector.checkObjectFieldValueTypes(ObjectStreamClass.java:2153)
>>>>
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectStreamClass.checkObjFieldValueTypes(ObjectStreamClass.java:1407)
>>>>
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectInputStream.defaultCheckFieldValues(ObjectInputStream.java:2371)
>>>>
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectInputStream.readSerialData(ObjectInputStream.java:2278)
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2087)
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectInputStream.readObject0(ObjectInputStream.java:1594)
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectInputStream.readArray(ObjectInputStream.java:1993)
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectInputStream.readObject0(ObjectInputStream.java:1588)
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectInputStream.defaultReadFields(ObjectInputStream.java:2355)
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectInputStream.readSerialData(ObjectInputStream.java:2249)
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2087)
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectInputStream.readObject0(ObjectInputStream.java:1594)
>>>> > > at
>>>> > > java.base/java.io <http://java.io>
>>>> >
>>>> <http://java.io
>>>> .ObjectInputStream.readObject(ObjectInputStream.java:430)
>>>> > > at
>>>> > >
>>>> >
>>>>
>>> test.SerializedLambdaTest$MyCode.deserialize(SerializedLambdaTest.java:35)
>>>> > > at
>>>> test.SerializedLambdaTest$MyCode.run(SerializedLambdaTest.java:51)
>>>> > > at
>>>> test.SerializedLambdaTest.main(SerializedLambdaTest.java:66)
>>>> > >
>>>> > >
>>>> > >
>>>> > > https://bugs.openjdk.java.net/browse/JDK-8008770
>>>> > >
>>>> > >
>>>> > > package test;
>>>> > >
>>>> > > import java.io.ByteArrayInputStream;
>>>> > > import java.io.ByteArrayOutputStream;
>>>> > > import java.io.IOException;
>>>> > > import java.io.InputStream;
>>>> > > import java.io.ObjectInputStream;
>>>> > > import java.io.ObjectOutputStream;
>>>> > > import java.io.Serializable;
>>>> > >
>>>> > > // from:
>>>> https://bugs.openjdk.java.net/browse/JDK-8008770
>>>> > > public class SerializedLambdaTest {
>>>> > > public interface SerializableRunnable extends
>>>> > Runnable,Serializable {
>>>> > > }
>>>> > > public static class MyCode implements
>>>> SerializableRunnable {
>>>> > > SerializableRunnable runnable2 = ()
>>>> > > -> {
>>>> > > System.out.println("HELLO"+this.getClass());
>>>> > > };
>>>> > > private byte[] serialize(Object o) {
>>>> > > ByteArrayOutputStream baos;
>>>> > > try (
>>>> > > ObjectOutputStream oos
>>>> > > = new ObjectOutputStream(baos =
>>>> new
>>>> > > ByteArrayOutputStream())) {
>>>> > > oos.writeObject(o);
>>>> > > } catch (IOException e) {
>>>> > > throw new RuntimeException(e);
>>>> > > }
>>>> > > return baos.toByteArray();
>>>> > > }
>>>> > > private <T> T deserialize(byte[] bytes) {
>>>> > > try (
>>>> > > ObjectInputStream ois
>>>> > > = new ObjectInputStream(new
>>>> > > ByteArrayInputStream(bytes))) {
>>>> > > return (T) ois.readObject();
>>>> > > } catch
>>>> (IOException|ClassNotFoundException
>>> e) {
>>>> > > throw new RuntimeException(e);
>>>> > > }
>>>> > > }
>>>> > > @Override
>>>> > > public void run() {
>>>> > > System.out.println(" this:
>>>> "+this);
>>>> > > SerializableRunnable deSerializedThis
>>>> > > = deserialize(serialize(this));
>>>> > > System.out.println(" deSerializedThis: "
>>>> > > +deSerializedThis);
>>>> > >
>>>> > > SerializableRunnable runnable = runnable2;
>>>> > > System.out.println(" runnable:
>>>> "+runnable);
>>>> > > SerializableRunnable deSerializedRunnable
>>>> > > =
>>>> deserialize(serialize(runnable));
>>>> > > System.out.println("deSerializedRunnable: "
>>>> > > +deSerializedRunnable);
>>>> > > }
>>>> > > }
>>>> > > public static void main(String[] args) throws
>>> Exception {
>>>> > > ClassLoader myCl = new MyClassLoader(
>>>> > >
>>> SerializedLambdaTest.class.getClassLoader()
>>>> > > );
>>>> > > Class<?> myCodeClass = Class.forName(
>>>> > >
>>>> SerializedLambdaTest.class.getName()+"$MyCode",
>>>> > > true,
>>>> > > myCl
>>>> > > );
>>>> > > Runnable myCode = (Runnable)
>>>> myCodeClass.newInstance();
>>>> > > myCode.run();
>>>> > > }
>>>> > > static class MyClassLoader extends ClassLoader {
>>>> > > MyClassLoader(ClassLoader parent) {
>>>> > > super(parent);
>>>> > > }
>>>> > > @Override
>>>> > > protected Class<?> loadClass(String
>>>> name,boolean
>>>> resolve)
>>>> > > throws ClassNotFoundException {
>>>> > > if (name.startsWith("test."))
>>>> > > synchronized
>>>> (getClassLoadingLock(name)) {
>>>> > > Class<?> c =
>>>> findLoadedClass(name);
>>>> > > if (c==null)
>>>> > > c = findClass(name);
>>>> > > if (resolve)
>>>> > > resolveClass(c);
>>>> > > return c;
>>>> > > }
>>>> > > else
>>>> > > return super.loadClass(name,resolve);
>>>> > > }
>>>> > > @Override
>>>> > > protected Class<?> findClass(String name)
>>>> throws
>>>> > > ClassNotFoundException {
>>>> > > String path =
>>>> name.replace('.','/').concat(".class");
>>>> > > try ( InputStream is =
>>>> getResourceAsStream(path)) {
>>>> > > if (is!=null) {
>>>> > > byte[] bytes = is.readAllBytes();
>>>> > > return
>>>> defineClass(name,bytes,0,bytes.length);
>>>> > > } else
>>>> > > throw new
>>> ClassNotFoundException(name);
>>>> > > } catch (IOException e) {
>>>> > > throw new
>>>> ClassNotFoundException(name,e);
>>>> > > }
>>>> > > }
>>>> > > }
>>>> > > }
>>>> > >
>>>> >
>>>>
>
More information about the core-libs-dev
mailing list