EsotericSoftware / kryo

Java binary serialization and cloning: fast, efficient, automatic
BSD 3-Clause "New" or "Revised" License
6.19k stars 823 forks source link

Java 14 records : how to deal with them? #735

Closed payne911 closed 3 years ago

payne911 commented 4 years ago

How does KryoNet work with Java 14's records? Is there a recommended custom Serializer?

I'm getting:

com.esotericsoftware.kryonet.KryoNetException: Error during deserialization.
        at com.esotericsoftware.kryonet.TcpConnection.readObject(TcpConnection.java:159)
        at com.esotericsoftware.kryonet.Server.update(Server.java:223)
        at com.esotericsoftware.kryonet.Server.run(Server.java:390)
        at java.base/java.lang.Thread.run(Thread.java:832)
Caused by: com.esotericsoftware.kryo.KryoException: Class cannot be created (missing no-arg constructor): com.marvelousbob.server.network.Ping
        at com.esotericsoftware.kryo.Kryo$DefaultInstantiatorStrategy.newInstantiatorOf(Kryo.java:1228)
        at com.esotericsoftware.kryo.Kryo.newInstantiator(Kryo.java:1049)
        at com.esotericsoftware.kryo.Kryo.newInstance(Kryo.java:1058)
        at com.esotericsoftware.kryo.serializers.FieldSerializer.create(FieldSerializer.java:547)
        at com.esotericsoftware.kryo.serializers.FieldSerializer.read(FieldSerializer.java:523)
        at com.esotericsoftware.kryo.Kryo.readClassAndObject(Kryo.java:764)
        at com.esotericsoftware.kryonet.KryoSerialization.read(KryoSerialization.java:73)
        at com.esotericsoftware.kryonet.TcpConnection.readObject(TcpConnection.java:157)
        ... 3 more

Using lombok's @NoArgsConstructor does not allow the compiler to finish its work:

error: constructor is not canonical, so its first statement must invoke another constructor
@NoArgsConstructor
^

And if I try with something like this:

public record Ping(long timeStamp) {

    public Ping() {
        this(System.currentTimeMillis());
    }
}

I get:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.esotericsoftware.reflectasm.AccessClassLoader (file:/C:/Users/payne/.gradle/caches/modules-2/files-2.1/com.esotericsoftware.kryo/kryo/2.24.0/c6b206e80cfd97e66a1364003724491c757b92f/kryo-2.24.0.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
WARNING: Please consider reporting this to the maintainers of com.esotericsoftware.reflectasm.AccessClassLoader
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
theigl commented 4 years ago

There is no built-in support for records and there most likely won't be until the next Java LTS release.

Serialization of records works very differently from "normal" classes:

Records are serialized differently than ordinary serializable or externalizable objects. The serialized form of a record object is a sequence of values derived from the final instance fields of the object. The stream format of a record object is the same as that of an ordinary object in the stream. During deserialization, if the local class equivalent of the specified stream class descriptor is a record class, then first the stream fields are read and reconstructed to serve as the record's component values; and second, a record object is created by invoking the record's canonical constructor with the component values as arguments (or the default value for component's type if a component value is absent from the stream).

http://cr.openjdk.java.net/~chegar/records/spec/records-serialization.03.html#serialization-of-records

You have a couple of options to deal with this:

  1. If the number of records you want to serialize is small, your best option is to write custom serializers for them that write all fields of the record and re-construct it by invoking the constructor.

  2. You can write a generic RecordSerializer that does exactly what the documentation above describes.

  3. You can fallback on the JavaSerializer for records by checking for Class.isRecord() in the serializer factory.

FrauBoes commented 4 years ago

Hi,

I work on the JDK and I’d be happy to help with supporting record types.

If you are planning on adding built-in support for records, I can share a custom serialiser I recently wrote. It’s an initial version that likely requires further review and discussion, but with about a year until the next Java LTS release, it could be a good start.

To give some more background: As @payne911 mentioned, in JDK 14 the FieldSerializer can actually handle the record, provided it has a no-arg constructor. There is an illegal access warning, but the read operation completes. However, starting in JDK 15, core reflection will no longer be able to mutate the fields of a record object. In this case, the following exception is thrown when the FieldSerializer is initialised:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.esotericsoftware.kryo.unsafe.UnsafeUtil (file:/…/.m2/repository/com/esotericsoftware/kryo/5.0.0-RC9/kryo-5.0.0-RC9.jar) to method sun.nio.ch.DirectBuffer.cleaner()
WARNING: Please consider reporting this to the maintainers of com.esotericsoftware.kryo.unsafe.UnsafeUtil
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

java.lang.UnsupportedOperationException: can't get field offset on a record (preview): private final int org.example.PointRecord.x

    at jdk.unsupported/sun.misc.Unsafe.objectFieldOffset(Unsafe.java:649)
    at com.esotericsoftware.kryo.serializers.UnsafeField$IntUnsafeField.<init>(UnsafeField.java:68)
    at com.esotericsoftware.kryo.serializers.CachedFields.newUnsafeField(CachedFields.java:199)
    at com.esotericsoftware.kryo.serializers.CachedFields.addField(CachedFields.java:156)
    at com.esotericsoftware.kryo.serializers.CachedFields.rebuild(CachedFields.java:99)
    at com.esotericsoftware.kryo.serializers.FieldSerializer.<init>(FieldSerializer.java:82)
    at com.esotericsoftware.kryo.SerializerFactory$FieldSerializerFactory.newSerializer(SerializerFactory.java:128)
    at com.esotericsoftware.kryo.SerializerFactory$FieldSerializerFactory.newSerializer(SerializerFactory.java:111)
    at com.esotericsoftware.kryo.Kryo.newDefaultSerializer(Kryo.java:398)
    at com.esotericsoftware.kryo.Kryo.getDefaultSerializer(Kryo.java:383)
    at com.esotericsoftware.kryo.Kryo.register(Kryo.java:412)
    at org.example.RecordSerializerTest.test(RecordSerializerTest.java:18)

As you already mentioned, record serialization differs from regular serialization, and with JDK 15 this will manifest even more. For records, construction should then proceed through the canonical constructor.

Again, I'd love to to help to make kryo work with records, so let me know if I can contribute in any way.

theigl commented 4 years ago

@FrauBoes: Thank you so much for looking into this!

It would be fantastic if you could share your custom serializer in a PR! That would be a great starting point to discuss the issue further. I'll see if I can adjust the build so we can compile against JDK14+.

theigl commented 4 years ago

For reference: https://github.com/EsotericSoftware/kryo/pull/766