Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
payne911 opened this issue Jun 9, 2020 · 4 comments · Fixed by #766 or #815
Closed

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

payne911 opened this issue Jun 9, 2020 · 4 comments · Fixed by #766 or #815

Comments

@payne911
Copy link

payne911 commented Jun 9, 2020

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
Copy link
Collaborator

theigl commented Jun 10, 2020

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
Copy link
Contributor

FrauBoes commented Sep 3, 2020

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
Copy link
Collaborator

theigl commented Sep 3, 2020

@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
Copy link
Collaborator

theigl commented Sep 4, 2020

For reference: #766

@theigl theigl linked a pull request Sep 14, 2020 that will close this issue
@theigl theigl linked a pull request Apr 1, 2021 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging a pull request may close this issue.

3 participants