From 9706cd6b2d5213976823015c3d1c9abc6236d5ce Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Mon, 21 Feb 2022 20:10:22 +0100 Subject: [PATCH 01/19] Experimental FieldSerializer for Records --- .../kryo/serializers/AsmField.java | 43 ++++++++++ .../kryo/serializers/CachedFields.java | 2 +- .../kryo/serializers/FieldSerializer.java | 46 ++++++++++- .../kryo/serializers/ReflectField.java | 82 ++++++++++++++++++- .../kryo/serializers/UnsafeField.java | 42 ++++++++++ .../serializers/RecordSerializerTest.java | 6 +- 6 files changed, 213 insertions(+), 8 deletions(-) diff --git a/src/com/esotericsoftware/kryo/serializers/AsmField.java b/src/com/esotericsoftware/kryo/serializers/AsmField.java index 32d544e74..888280f74 100644 --- a/src/com/esotericsoftware/kryo/serializers/AsmField.java +++ b/src/com/esotericsoftware/kryo/serializers/AsmField.java @@ -74,6 +74,13 @@ public void read (Input input, Object object) { access.setInt(object, accessIndex, input.readInt()); } + public Object read(Input input) { + if (varEncoding) + return input.readVarInt(false); + else + return input.readInt(); + } + public void copy (Object original, Object copy) { access.setInt(copy, accessIndex, access.getInt(original, accessIndex)); } @@ -92,6 +99,10 @@ public void read (Input input, Object object) { access.setFloat(object, accessIndex, input.readFloat()); } + public Object read(Input input) { + return input.readFloat(); + } + public void copy (Object original, Object copy) { access.setFloat(copy, accessIndex, access.getFloat(original, accessIndex)); } @@ -110,6 +121,10 @@ public void read (Input input, Object object) { access.setShort(object, accessIndex, input.readShort()); } + public Object read(Input input) { + return input.readShort(); + } + public void copy (Object original, Object copy) { access.setShort(copy, accessIndex, access.getShort(original, accessIndex)); } @@ -128,6 +143,10 @@ public void read (Input input, Object object) { access.setByte(object, accessIndex, input.readByte()); } + public Object read(Input input) { + return input.readByte(); + } + public void copy (Object original, Object copy) { access.setByte(copy, accessIndex, access.getByte(original, accessIndex)); } @@ -146,6 +165,10 @@ public void read (Input input, Object object) { access.setBoolean(object, accessIndex, input.readBoolean()); } + public Object read(Input input) { + return input.readBoolean(); + } + public void copy (Object original, Object copy) { access.setBoolean(copy, accessIndex, access.getBoolean(original, accessIndex)); } @@ -164,6 +187,10 @@ public void read (Input input, Object object) { access.setChar(object, accessIndex, input.readChar()); } + public Object read(Input input) { + return input.readChar(); + } + public void copy (Object original, Object copy) { access.setChar(copy, accessIndex, access.getChar(original, accessIndex)); } @@ -188,6 +215,14 @@ public void read (Input input, Object object) { access.setLong(object, accessIndex, input.readLong()); } + public Object read(Input input) { + if (varEncoding) { + return input.readVarLong(false); + } else { + return input.readLong(); + } + } + public void copy (Object original, Object copy) { access.setLong(copy, accessIndex, access.getLong(original, accessIndex)); } @@ -206,6 +241,10 @@ public void read (Input input, Object object) { access.setDouble(object, accessIndex, input.readDouble()); } + public Object read(Input input) { + return input.readDouble(); + } + public void copy (Object original, Object copy) { access.setDouble(copy, accessIndex, access.getDouble(original, accessIndex)); } @@ -224,6 +263,10 @@ public void read (Input input, Object object) { access.set(object, accessIndex, input.readString()); } + public Object read(Input input) { + return input.readString(); + } + public void copy (Object original, Object copy) { access.set(copy, accessIndex, access.getString(original, accessIndex)); } diff --git a/src/com/esotericsoftware/kryo/serializers/CachedFields.java b/src/com/esotericsoftware/kryo/serializers/CachedFields.java index 5e2d43af8..acaa37aad 100644 --- a/src/com/esotericsoftware/kryo/serializers/CachedFields.java +++ b/src/com/esotericsoftware/kryo/serializers/CachedFields.java @@ -152,7 +152,7 @@ private void addField (Field field, boolean asm, ArrayList fields, } CachedField cachedField; - if (unsafe) + if (unsafe && !serializer.getType().isRecord()) cachedField = newUnsafeField(field, fieldClass, genericType); else if (accessIndex != -1) { cachedField = newAsmField(field, fieldClass, genericType); diff --git a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java index 0ff0719c8..70528f684 100644 --- a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java @@ -37,7 +37,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; +import java.util.Arrays; /** Serializes objects using direct field assignment. FieldSerializer is generic and can serialize most classes without any * configuration. All non-public fields are written and read by default, so it is important to evaluate each class that will be @@ -119,14 +121,24 @@ public void write (Kryo kryo, Output output, T object) { public T read (Kryo kryo, Input input, Class type) { int pop = pushTypeVariables(); - T object = create(kryo, input, type); - kryo.reference(object); + T object = null; + final boolean isRecord = type.isRecord(); + if (!isRecord) { + object = create(kryo, input, type); + kryo.reference(object); + } CachedField[] fields = cachedFields.fields; + Object[] values = new Object[fields.length];; for (int i = 0, n = fields.length; i < n; i++) { if (TRACE) log("Read", fields[i], input.position()); try { - fields[i].read(input, object); + final CachedField field = fields[i]; + if (object != null) { + field.read(input, object); + } else { + values[i] = field.read(input); + } } catch (KryoException e) { throw e; } catch (OutOfMemoryError | Exception e) { @@ -134,10 +146,36 @@ public T read (Kryo kryo, Input input, Class type) { } } + if (isRecord) { + final Class[] objects = Arrays.stream(fields).map(f -> f.field.getType()).toArray(Class[]::new); + object = invokeCanonicalConstructor(type, objects, values); + kryo.reference(object); + } + popTypeVariables(pop); return object; } + /** Invokes the canonical constructor of a record class with the given argument values. */ + private static T invokeCanonicalConstructor (Class recordType, + Class[] paramTypes, + Object[] args) { + try { + Constructor canonicalConstructor; + try { + canonicalConstructor = recordType.getConstructor(paramTypes); + } catch (NoSuchMethodException e) { + canonicalConstructor = recordType.getDeclaredConstructor(paramTypes); + canonicalConstructor.setAccessible(true); + } + return canonicalConstructor.newInstance(args); + } catch (Throwable t) { + KryoException ex = new KryoException(t); + ex.addTrace("Could not construct type (" + recordType.getName() + ")"); + throw ex; + } + } + /** Prepares the type variables for the serialized type. Must be balanced with {@link #popTypeVariables(int)} if >0 is * returned. */ protected int pushTypeVariables () { @@ -345,6 +383,8 @@ public String toString () { public abstract void read (Input input, Object object); + public abstract Object read (Input input); + public abstract void copy (Object original, Object copy); } diff --git a/src/com/esotericsoftware/kryo/serializers/ReflectField.java b/src/com/esotericsoftware/kryo/serializers/ReflectField.java index ec0818774..eb0a7fdb1 100644 --- a/src/com/esotericsoftware/kryo/serializers/ReflectField.java +++ b/src/com/esotericsoftware/kryo/serializers/ReflectField.java @@ -50,7 +50,7 @@ public Object get (Object object) throws IllegalAccessException { public void set (Object object, Object value) throws IllegalAccessException { field.set(object, value); } - + public void write (Output output, Object object) { Kryo kryo = fieldSerializer.kryo; try { @@ -148,6 +148,48 @@ public void read (Input input, Object object) { } } + public Object read(Input input) { + Kryo kryo = fieldSerializer.kryo; + try { + Object value; + + Serializer serializer = this.serializer; + Class concreteType = resolveFieldClass(); + if (concreteType == null) { + // The concrete type of the field is unknown, read the class first. + Registration registration = kryo.readClass(input); + if (registration == null) { + return null; + } + if (serializer == null) serializer = registration.getSerializer(); + kryo.getGenerics().pushGenericType(genericType); + value = kryo.readObject(input, registration.getType(), serializer); + } else { + if (serializer == null) { + serializer = kryo.getSerializer(concreteType); + // The concrete type of the field is known, always use the same serializer. + if (valueClass != null && reuseSerializer) this.serializer = serializer; + } + kryo.getGenerics().pushGenericType(genericType); + if (canBeNull) + value = kryo.readObjectOrNull(input, concreteType, serializer); + else + value = kryo.readObject(input, concreteType, serializer); + } + kryo.getGenerics().popGenericType(); + + + return value; + } catch (KryoException ex) { + ex.addTrace(name + " (" + fieldSerializer.type.getName() + ")"); + throw ex; + } catch (Throwable t) { + KryoException ex = new KryoException(t); + ex.addTrace(name + " (" + fieldSerializer.type.getName() + ")"); + throw ex; + } + } + Class resolveFieldClass () { if (valueClass == null) { Class fieldClass = genericType.resolve(fieldSerializer.kryo.getGenerics()); @@ -202,6 +244,13 @@ public void read (Input input, Object object) { } } + public Object read (Input input) { + if (varEncoding) + return input.readVarInt(false); + else + return input.readInt(); + } + public void copy (Object original, Object copy) { try { field.setInt(copy, field.getInt(original)); @@ -238,6 +287,10 @@ public void read (Input input, Object object) { } } + public Object read (Input input) { + return input.readFloat(); + } + public void copy (Object original, Object copy) { try { field.setFloat(copy, field.getFloat(original)); @@ -274,6 +327,10 @@ public void read (Input input, Object object) { } } + public Object read (Input input) { + return input.readShort(); + } + public void copy (Object original, Object copy) { try { field.setShort(copy, field.getShort(original)); @@ -310,6 +367,10 @@ public void read (Input input, Object object) { } } + public Object read (Input input) { + return input.readByte(); + } + public void copy (Object original, Object copy) { try { field.setByte(copy, field.getByte(original)); @@ -346,6 +407,10 @@ public void read (Input input, Object object) { } } + public Object read(Input input) { + return input.readBoolean(); + } + public void copy (Object original, Object copy) { try { field.setBoolean(copy, field.getBoolean(original)); @@ -382,6 +447,10 @@ public void read (Input input, Object object) { } } + public Object read(Input input) { + return input.readChar(); + } + public void copy (Object original, Object copy) { try { field.setChar(copy, field.getChar(original)); @@ -424,6 +493,13 @@ public void read (Input input, Object object) { } } + public Object read(Input input) { + if (varEncoding) + return input.readVarLong(false); + else + return input.readLong(); + } + public void copy (Object original, Object copy) { try { field.setLong(copy, field.getLong(original)); @@ -460,6 +536,10 @@ public void read (Input input, Object object) { } } + public Object read(Input input) { + return input.readDouble(); + } + public void copy (Object original, Object copy) { try { field.setDouble(copy, field.getDouble(original)); diff --git a/src/com/esotericsoftware/kryo/serializers/UnsafeField.java b/src/com/esotericsoftware/kryo/serializers/UnsafeField.java index a129eef7e..c1cc68797 100644 --- a/src/com/esotericsoftware/kryo/serializers/UnsafeField.java +++ b/src/com/esotericsoftware/kryo/serializers/UnsafeField.java @@ -79,6 +79,13 @@ public void read (Input input, Object object) { unsafe.putInt(object, offset, input.readInt()); } + public Object read(Input input) { + if (varEncoding) + return input.readVarInt(false); + else + return input.readInt(); + } + public void copy (Object original, Object copy) { unsafe.putInt(copy, offset, unsafe.getInt(original, offset)); } @@ -98,6 +105,10 @@ public void read (Input input, Object object) { unsafe.putFloat(object, offset, input.readFloat()); } + public Object read(Input input) { + return input.readFloat(); + } + public void copy (Object original, Object copy) { unsafe.putFloat(copy, offset, unsafe.getFloat(original, offset)); } @@ -117,6 +128,10 @@ public void read (Input input, Object object) { unsafe.putShort(object, offset, input.readShort()); } + public Object read(Input input) { + return input.readShort(); + } + public void copy (Object original, Object copy) { unsafe.putShort(copy, offset, unsafe.getShort(original, offset)); } @@ -136,6 +151,10 @@ public void read (Input input, Object object) { unsafe.putByte(object, offset, input.readByte()); } + public Object read(Input input) { + return input.readShort(); + } + public void copy (Object original, Object copy) { unsafe.putByte(copy, offset, unsafe.getByte(original, offset)); } @@ -155,6 +174,10 @@ public void read (Input input, Object object) { unsafe.putBoolean(object, offset, input.readBoolean()); } + public Object read(Input input) { + return input.readShort(); + } + public void copy (Object original, Object copy) { unsafe.putBoolean(copy, offset, unsafe.getBoolean(original, offset)); } @@ -174,6 +197,10 @@ public void read (Input input, Object object) { unsafe.putChar(object, offset, input.readChar()); } + public Object read(Input input) { + return input.readShort(); + } + public void copy (Object original, Object copy) { unsafe.putChar(copy, offset, unsafe.getChar(original, offset)); } @@ -199,6 +226,13 @@ public void read (Input input, Object object) { unsafe.putLong(object, offset, input.readLong()); } + public Object read (Input input) { + if (varEncoding) + return input.readVarLong(false); + else + return input.readLong(); + } + public void copy (Object original, Object copy) { unsafe.putLong(copy, offset, unsafe.getLong(original, offset)); } @@ -218,6 +252,10 @@ public void read (Input input, Object object) { unsafe.putDouble(object, offset, input.readDouble()); } + public Object read(Input input) { + return input.readDouble(); + } + public void copy (Object original, Object copy) { unsafe.putDouble(copy, offset, unsafe.getDouble(original, offset)); } @@ -237,6 +275,10 @@ public void read (Input input, Object object) { unsafe.putObject(object, offset, input.readString()); } + public Object read(Input input) { + return input.readString(); + } + public void copy (Object original, Object copy) { unsafe.putObject(copy, offset, unsafe.getObject(original, offset)); } diff --git a/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java b/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java index 24a3fd74c..89bfd2dd1 100644 --- a/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java +++ b/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java @@ -45,10 +45,10 @@ public record RecordRectangle (String height, int width, long x, double y) { } @Test public void testBasicRecord() { - kryo.register(RecordRectangle.class); + kryo.register(RecordRectangle.class, new FieldSerializer<>(kryo, RecordRectangle.class)); final var r1 = new RecordRectangle("one", 2, 3L, 4.0); - final var output = new Output(32); + final var output = new Output(64); kryo.writeObject(output, r1); final var input = new Input(output.getBuffer(), 0, output.position()); final var r2 = kryo.readObject(input, RecordRectangle.class); @@ -112,7 +112,7 @@ public RecordWithConstructor(String height) { @Test public void testRecordWithConstructor() { - kryo.register(RecordWithConstructor.class); + kryo.register(RecordWithConstructor.class, new FieldSerializer<>(kryo, RecordWithConstructor.class)); final var r1 = new RecordWithConstructor("ten"); final var output = new Output(32); From 0a7394876175dfe3360bd9b7399fccdad91f210b Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Thu, 24 Feb 2022 18:43:04 +0100 Subject: [PATCH 02/19] Experimental FieldSerializer for Records --- src/com/esotericsoftware/kryo/Kryo.java | 3 +- .../kryo/serializers/CachedFields.java | 11 ++ .../kryo/serializers/DefaultSerializers.java | 109 +++++++++++++++++- .../kryo/serializers/FieldSerializer.java | 7 +- 4 files changed, 125 insertions(+), 5 deletions(-) diff --git a/src/com/esotericsoftware/kryo/Kryo.java b/src/com/esotericsoftware/kryo/Kryo.java index fa0434023..0701b8572 100644 --- a/src/com/esotericsoftware/kryo/Kryo.java +++ b/src/com/esotericsoftware/kryo/Kryo.java @@ -81,7 +81,6 @@ import com.esotericsoftware.kryo.serializers.ImmutableCollectionsSerializers; import com.esotericsoftware.kryo.serializers.MapSerializer; import com.esotericsoftware.kryo.serializers.OptionalSerializers; -import com.esotericsoftware.kryo.serializers.RecordSerializer; import com.esotericsoftware.kryo.serializers.TimeSerializers; import com.esotericsoftware.kryo.util.DefaultClassResolver; import com.esotericsoftware.kryo.util.DefaultGenerics; @@ -232,7 +231,7 @@ public Kryo (ClassResolver classResolver, ReferenceResolver referenceResolver) { ImmutableCollectionsSerializers.addDefaultSerializers(this); // Add RecordSerializer if JDK 14+ available if (isClassAvailable("java.lang.Record")) { - addDefaultSerializer("java.lang.Record", RecordSerializer.class); + //addDefaultSerializer("java.lang.Record", RecordSerializer.class); } lowPriorityDefaultSerializerCount = defaultSerializers.size(); diff --git a/src/com/esotericsoftware/kryo/serializers/CachedFields.java b/src/com/esotericsoftware/kryo/serializers/CachedFields.java index acaa37aad..9457f0a80 100644 --- a/src/com/esotericsoftware/kryo/serializers/CachedFields.java +++ b/src/com/esotericsoftware/kryo/serializers/CachedFields.java @@ -62,6 +62,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.lang.reflect.RecordComponent; import java.security.AccessControlException; import java.util.ArrayList; import java.util.Arrays; @@ -183,6 +184,16 @@ else if (accessIndex != -1) { "Cached " + fieldClass.getSimpleName() + " field: " + field.getName() + " (" + className(declaringClass) + ")"); } + final RecordComponent[] recordComponents = serializer.getType().getRecordComponents(); + if (recordComponents != null) { + for (int i = 0; i < recordComponents.length; i++) { + RecordComponent recordComponent = recordComponents[i]; + if (recordComponent.getName().equals(field.getName())) { + cachedField.index = i; + } + } + } + applyAnnotations(cachedField); if (isTransient) { diff --git a/src/com/esotericsoftware/kryo/serializers/DefaultSerializers.java b/src/com/esotericsoftware/kryo/serializers/DefaultSerializers.java index 6d1fde2fc..da41efeaa 100644 --- a/src/com/esotericsoftware/kryo/serializers/DefaultSerializers.java +++ b/src/com/esotericsoftware/kryo/serializers/DefaultSerializers.java @@ -34,6 +34,7 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; import java.nio.charset.Charset; import java.sql.Time; @@ -58,7 +59,13 @@ import java.util.TimeZone; import java.util.TreeMap; import java.util.TreeSet; +import java.util.UUID; import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; /** Contains many serializer classes that are provided by {@link Kryo#addDefaultSerializer(Class, Class) default}. * @author Nathan Sweet */ @@ -874,6 +881,7 @@ public List copy (Kryo kryo, List original) { } } + /** Serializer for {@link BitSet} */ public static class BitSetSerializer extends Serializer { public void write (Kryo kryo, Output output, BitSet set) { long[] values = set.toLongArray(); @@ -884,12 +892,109 @@ public void write (Kryo kryo, Output output, BitSet set) { public BitSet read (Kryo kryo, Input input, Class type) { int length = input.readVarInt(true); long[] values = input.readLongs(length); - BitSet set = BitSet.valueOf(values); - return set; + return BitSet.valueOf(values); } public BitSet copy (Kryo kryo, BitSet original) { return BitSet.valueOf(original.toLongArray()); } } + + /** Serializer for {@link Pattern} */ + public static class PatternSerializer extends ImmutableSerializer { + public void write (Kryo kryo, Output output, Pattern pattern) { + output.writeString(pattern.pattern()); + output.writeInt(pattern.flags(), true); + } + + public Pattern read (Kryo kryo, Input input, Class patternClass) { + String regex = input.readString(); + int flags = input.readInt(true); + return Pattern.compile(regex, flags); + } + } + + /** Serializer for {@link URI} */ + public static class URISerializer extends ImmutableSerializer { + public void write (Kryo kryo, Output output, URI uri) { + output.writeString(uri.toString()); + } + + public URI read (Kryo kryo, Input input, Class uriClass) { + return URI.create(input.readString()); + } + } + + /** Serializer for {@link UUID} */ + public static class UUIDSerializer extends ImmutableSerializer { + public void write (Kryo kryo, Output output, UUID uuid) { + output.writeLong(uuid.getMostSignificantBits()); + output.writeLong(uuid.getLeastSignificantBits()); + } + + public UUID read (final Kryo kryo, final Input input, final Class uuidClass) { + return new UUID(input.readLong(), input.readLong()); + } + } + + /** Serializer for {@link AtomicBoolean} */ + public static class AtomicBooleanSerializer extends Serializer { + public void write (Kryo kryo, Output output, AtomicBoolean object) { + output.writeBoolean(object.get()); + } + + public AtomicBoolean read (Kryo kryo, Input input, Class type) { + return new AtomicBoolean(input.readBoolean()); + } + + public AtomicBoolean copy (Kryo kryo, AtomicBoolean original) { + return new AtomicBoolean(original.get()); + } + } + + /** Serializer for {@link AtomicInteger} */ + public static class AtomicIntegerSerializer extends Serializer { + public void write (Kryo kryo, Output output, AtomicInteger object) { + output.writeInt(object.get()); + } + + public AtomicInteger read (Kryo kryo, Input input, Class type) { + return new AtomicInteger(input.readInt()); + } + + public AtomicInteger copy (Kryo kryo, AtomicInteger original) { + return new AtomicInteger(original.get()); + } + } + + /** Serializer for {@link AtomicLong} */ + public static class AtomicLongSerializer extends Serializer { + public void write (Kryo kryo, Output output, AtomicLong object) { + output.writeLong(object.get()); + } + + public AtomicLong read (Kryo kryo, Input input, Class type) { + return new AtomicLong(input.readLong()); + } + + public AtomicLong copy (Kryo kryo, AtomicLong original) { + return new AtomicLong(original.get()); + } + } + + /** Serializer for {@link AtomicReference} */ + public static class AtomicReferenceSerializer extends Serializer { + public void write (Kryo kryo, Output output, AtomicReference object) { + kryo.writeClassAndObject(output, object.get()); + } + + public AtomicReference read (Kryo kryo, Input input, Class type) { + final Object value = kryo.readClassAndObject(input); + return new AtomicReference(value); + } + + public AtomicReference copy (Kryo kryo, AtomicReference original) { + return new AtomicReference<>(kryo.copy(original.get())); + } + } } diff --git a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java index 70528f684..11f839841 100644 --- a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java @@ -147,7 +147,9 @@ public T read (Kryo kryo, Input input, Class type) { } if (isRecord) { - final Class[] objects = Arrays.stream(fields).map(f -> f.field.getType()).toArray(Class[]::new); + final Class[] objects = Arrays.stream(fields) + .map(f -> f.field.getType()) + .toArray(Class[]::new); object = invokeCanonicalConstructor(type, objects, values); kryo.reference(object); } @@ -280,6 +282,9 @@ public abstract static class CachedField { // For AsmField. FieldAccess access; int accessIndex = -1; + + // For Records + int index; // For UnsafeField. long offset; From b945876ba22408e031156b460f8a59264d714510 Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Sun, 13 Nov 2022 15:35:57 +0100 Subject: [PATCH 03/19] Experimental FieldSerializer for Records --- .../esotericsoftware/kryo/serializers/FieldSerializer.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java index 11f839841..f619c511c 100644 --- a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java @@ -40,6 +40,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Arrays; +import java.util.Comparator; /** Serializes objects using direct field assignment. FieldSerializer is generic and can serialize most classes without any * configuration. All non-public fields are written and read by default, so it is important to evaluate each class that will be @@ -137,7 +138,7 @@ public T read (Kryo kryo, Input input, Class type) { if (object != null) { field.read(input, object); } else { - values[i] = field.read(input); + values[field.index] = field.read(input); } } catch (KryoException e) { throw e; @@ -148,6 +149,7 @@ public T read (Kryo kryo, Input input, Class type) { if (isRecord) { final Class[] objects = Arrays.stream(fields) + .sorted(Comparator.comparing(f -> f.index)) .map(f -> f.field.getType()) .toArray(Class[]::new); object = invokeCanonicalConstructor(type, objects, values); From 10681fcdd93d08e88021bda3916a4b4b5a557be6 Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Sun, 13 Nov 2022 15:47:41 +0100 Subject: [PATCH 04/19] Experimental FieldSerializer for Records --- .../benchmarks/RecordSerializerBenchmark.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/RecordSerializerBenchmark.java diff --git a/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/RecordSerializerBenchmark.java b/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/RecordSerializerBenchmark.java new file mode 100644 index 000000000..089ae17c4 --- /dev/null +++ b/benchmarks/src/main/java/com/esotericsoftware/kryo/benchmarks/RecordSerializerBenchmark.java @@ -0,0 +1,99 @@ +/* Copyright (c) 2008-2022, Nathan Sweet + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided with the distribution. + * - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT + * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +package com.esotericsoftware.kryo.benchmarks; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.Serializer; +import com.esotericsoftware.kryo.SerializerFactory.CompatibleFieldSerializerFactory; +import com.esotericsoftware.kryo.SerializerFactory.TaggedFieldSerializerFactory; +import com.esotericsoftware.kryo.benchmarks.data.Image; +import com.esotericsoftware.kryo.benchmarks.data.Image.Size; +import com.esotericsoftware.kryo.benchmarks.data.Media; +import com.esotericsoftware.kryo.benchmarks.data.Media.Player; +import com.esotericsoftware.kryo.benchmarks.data.MediaContent; +import com.esotericsoftware.kryo.benchmarks.data.Sample; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; +import com.esotericsoftware.kryo.serializers.CollectionSerializer; +import com.esotericsoftware.kryo.serializers.FieldSerializer; +import com.esotericsoftware.kryo.serializers.RecordSerializer; +import com.esotericsoftware.kryo.serializers.VersionFieldSerializer; + +import java.util.ArrayList; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; + +public class RecordSerializerBenchmark { + @Benchmark + public void field (FieldSerializerState state) { + state.roundTrip(); + } + + @Benchmark + public void record (RecordSerializerState state) { + state.roundTrip(); + } + + @State(Scope.Thread) + static public abstract class BenchmarkState { + @Param({"true", "false"}) public boolean references; + + final Kryo kryo = new Kryo(); + final Output output = new Output(1024 * 512); + final Input input = new Input(output.getBuffer()); + Object object; + + @Setup(Level.Trial) + public void setup () { + object = new RecordRectangle("2134324", 10, 10L, 20D); + kryo.setReferences(references); + } + + public void roundTrip () { + output.setPosition(0); + kryo.writeObject(output, object); + input.setPosition(0); + input.setLimit(output.position()); + kryo.readObject(input, object.getClass()); + } + } + + public record RecordRectangle (String height, int width, long x, double y) { } + + static public class FieldSerializerState extends BenchmarkState { + public void setup () { + kryo.setDefaultSerializer(FieldSerializer.class); + kryo.register(RecordRectangle.class); + super.setup(); + } + } + + static public class RecordSerializerState extends BenchmarkState { + public void setup () { + kryo.register(RecordRectangle.class, new RecordSerializer<>()); + super.setup(); + } + } +} From e8563aac57dfad9e3898ecf66cacb1f75c3db157 Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Sun, 13 Nov 2022 18:43:05 +0100 Subject: [PATCH 05/19] Experimental FieldSerializer for Records --- .../kryo/serializers/FieldSerializer.java | 41 +++++++++++++++---- .../serializers/TaggedFieldSerializer.java | 27 ++++++++++-- .../serializers/VersionFieldSerializer.java | 28 +++++++++++-- .../TaggedFieldSerializerTest.java | 14 +++++-- .../VersionedFieldSerializerTest.java | 10 ++++- 5 files changed, 103 insertions(+), 17 deletions(-) diff --git a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java index f619c511c..5c079f847 100644 --- a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java @@ -130,7 +130,7 @@ public T read (Kryo kryo, Input input, Class type) { } CachedField[] fields = cachedFields.fields; - Object[] values = new Object[fields.length];; + Object[] values = null; for (int i = 0, n = fields.length; i < n; i++) { if (TRACE) log("Read", fields[i], input.position()); try { @@ -138,6 +138,7 @@ public T read (Kryo kryo, Input input, Class type) { if (object != null) { field.read(input, object); } else { + if (values == null) values = new Object[fields.length]; values[field.index] = field.read(input); } } catch (KryoException e) { @@ -161,7 +162,7 @@ public T read (Kryo kryo, Input input, Class type) { } /** Invokes the canonical constructor of a record class with the given argument values. */ - private static T invokeCanonicalConstructor (Class recordType, + static T invokeCanonicalConstructor (Class recordType, Class[] paramTypes, Object[] args) { try { @@ -264,11 +265,37 @@ protected T createCopy (Kryo kryo, T original) { } public T copy (Kryo kryo, T original) { - T copy = createCopy(kryo, original); - kryo.reference(copy); - - for (int i = 0, n = cachedFields.copyFields.length; i < n; i++) - cachedFields.copyFields[i].copy(original, copy); + T copy = null; + final boolean isRecord = original.getClass().isRecord(); + if (!isRecord) { + copy = createCopy(kryo, original); + kryo.reference(copy); + } + + Object[] values = null; + final CachedField[] copyFields = cachedFields.copyFields; + for (int i = 0, n = copyFields.length; i < n; i++) { + final CachedField field = copyFields[i]; + if (copy != null) { + field.copy(original, copy); + } else { + if (values == null) values = new Object[copyFields.length]; + try { + values[field.index] = field.getField().get(original); + } catch (IllegalAccessException e) { + throw new KryoException(e); + } + } + } + + if (isRecord) { + final Class[] objects = Arrays.stream(copyFields) + .sorted(Comparator.comparing(f -> f.index)) + .map(f -> f.field.getType()) + .toArray(Class[]::new); + copy = (T) invokeCanonicalConstructor(type, objects, values); + kryo.reference(copy); + } return copy; } diff --git a/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java index 18a760b7d..dabeaec44 100644 --- a/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java @@ -38,6 +38,8 @@ import java.lang.annotation.Target; import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; /** Serializes objects using direct field assignment for fields that have a @Tag(int) annotation, providing backward * compatibility and optional forward compatibility. This means fields can be added or renamed and optionally removed without @@ -174,8 +176,12 @@ public T read (Kryo kryo, Input input, Class type) { int pop = pushTypeVariables(); - T object = create(kryo, input, type); - kryo.reference(object); + T object = null; + final boolean isRecord = type.isRecord(); + if (!isRecord) { + object = create(kryo, input, type); + kryo.reference(object); + } boolean chunked = config.chunked, readUnknownTagData = config.readUnknownTagData; Input fieldInput; @@ -185,6 +191,7 @@ public T read (Kryo kryo, Input input, Class type) { else fieldInput = input; IntMap readTags = this.readTags; + Object[] values = null; for (int i = 0; i < fieldCount; i++) { int tag = input.readVarInt(true); CachedField cachedField = readTags.get(tag); @@ -231,10 +238,24 @@ public T read (Kryo kryo, Input input, Class type) { } if (TRACE) log("Read", cachedField, input.position()); - cachedField.read(fieldInput, object); + if (object != null) { + cachedField.read(fieldInput, object); + } else { + if (values == null) values = new Object[fieldCount]; + values[cachedField.index] = cachedField.read(fieldInput); + } if (chunked) inputChunked.nextChunk(); } + if (isRecord) { + final Class[] objects = readTags.values().toList().stream() + .sorted(Comparator.comparing(f -> f.index)) + .map(f -> f.field.getType()) + .toArray(Class[]::new); + object = invokeCanonicalConstructor(type, objects, values); + kryo.reference(object); + } + popTypeVariables(pop); return object; } diff --git a/src/com/esotericsoftware/kryo/serializers/VersionFieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/VersionFieldSerializer.java index d87fdb1ba..64fa02c3e 100644 --- a/src/com/esotericsoftware/kryo/serializers/VersionFieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/VersionFieldSerializer.java @@ -32,6 +32,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Comparator; /** Serializes objects using direct field assignment, providing backward compatibility with minimal overhead. This means fields * can be added without invalidating previously serialized bytes. Removing, renaming, or changing the type of a field is not @@ -117,10 +119,15 @@ public T read (Kryo kryo, Input input, Class type) { int pop = pushTypeVariables(); - T object = create(kryo, input, type); - kryo.reference(object); + T object = null; + final boolean isRecord = type.isRecord(); + if (!isRecord) { + object = create(kryo, input, type); + kryo.reference(object); + } CachedField[] fields = cachedFields.fields; + Object[] values = null; for (int i = 0, n = fields.length; i < n; i++) { // Field is not present in input, skip it. if (fieldVersion[i] > version) { @@ -128,7 +135,22 @@ public T read (Kryo kryo, Input input, Class type) { continue; } if (TRACE) log("Read", fields[i], input.position()); - fields[i].read(input, object); + final CachedField field = fields[i]; + if (object != null) { + field.read(input, object); + } else { + if (values == null) values = new Object[fields.length]; + values[field.index] = field.read(input); + } + } + + if (isRecord) { + final Class[] objects = Arrays.stream(fields) + .sorted(Comparator.comparing(f -> f.index)) + .map(f -> f.field.getType()) + .toArray(Class[]::new); + object = invokeCanonicalConstructor(type, objects, values); + kryo.reference(object); } popTypeVariables(pop); diff --git a/test/com/esotericsoftware/kryo/serializers/TaggedFieldSerializerTest.java b/test/com/esotericsoftware/kryo/serializers/TaggedFieldSerializerTest.java index d8b11fdfe..f2a57789b 100644 --- a/test/com/esotericsoftware/kryo/serializers/TaggedFieldSerializerTest.java +++ b/test/com/esotericsoftware/kryo/serializers/TaggedFieldSerializerTest.java @@ -49,10 +49,12 @@ void testTaggedFieldSerializer () { object1.other = new AnotherClass(); object1.other.value = "meow"; object1.ignored = 32; + object1.record = new RecordClass("1", 1, 1L, 1d); kryo.setDefaultSerializer(TaggedFieldSerializer.class); kryo.register(TestClass.class); kryo.register(AnotherClass.class); - TestClass object2 = roundTrip(57, object1); + kryo.register(RecordClass.class); + TestClass object2 = roundTrip(77, object1); assertEquals(0, object2.ignored); } @@ -67,7 +69,8 @@ void testAddedField () { serializer.removeField("text"); kryo.register(TestClass.class, serializer); kryo.register(AnotherClass.class, new TaggedFieldSerializer(kryo, AnotherClass.class)); - roundTrip(39, object1); + kryo.register(RecordClass.class, new TaggedFieldSerializer(kryo, AnotherClass.class)); + roundTrip(43, object1); kryo.register(TestClass.class, new TaggedFieldSerializer(kryo, TestClass.class)); Object object2 = kryo.readClassAndObject(input); @@ -100,6 +103,7 @@ void testForwardCompatibility () { factory.getConfig().setChunkedEncoding(true); kryo.setDefaultSerializer(factory); kryo.register(TestClass.class); + kryo.register(RecordClass.class); kryo.register(Object[].class); TaggedFieldSerializer futureSerializer = new TaggedFieldSerializer(kryo, FutureClass.class); futureSerializer.getTaggedFieldSerializerConfig().setChunkedEncoding(true); @@ -196,7 +200,8 @@ public static class TestClass { @Tag(3) public TestClass child; @Tag(4) public int zzz = 123; @Tag(5) public AnotherClass other; - @Tag(6) @Deprecated public int ignored; + @Tag(6) public RecordClass record; + @Tag(7) @Deprecated public int ignored; public boolean equals (Object obj) { if (this == obj) return true; @@ -212,6 +217,7 @@ public boolean equals (Object obj) { if (other.text != null) return false; } else if (!text.equals(other.text)) return false; if (zzz != other.zzz) return false; + if (!Objects.equals(record, other.record)) return false; return true; } } @@ -224,6 +230,8 @@ public static class AnotherClass { @Tag(1) String value; } + public record RecordClass(@Tag(0) String height, @Tag(1) int width, @Tag(2) long x, @Tag(3) double y) { } + private static class FutureClass { @Tag(0) public Integer value; @Tag(1) public FutureClass2 futureClass2; diff --git a/test/com/esotericsoftware/kryo/serializers/VersionedFieldSerializerTest.java b/test/com/esotericsoftware/kryo/serializers/VersionedFieldSerializerTest.java index d5656b37f..68b862bcc 100644 --- a/test/com/esotericsoftware/kryo/serializers/VersionedFieldSerializerTest.java +++ b/test/com/esotericsoftware/kryo/serializers/VersionedFieldSerializerTest.java @@ -26,6 +26,8 @@ import org.junit.jupiter.api.Test; +import java.util.Objects; + class VersionedFieldSerializerTest extends KryoTestCase { { supportsCopy = true; @@ -38,16 +40,18 @@ void testVersionFieldSerializer () { object1.child = null; object1.other = new AnotherClass(); object1.other.value = "meow"; + object1.record = new RecordClass("1", 2, 3l, 4d); kryo.setDefaultSerializer(VersionFieldSerializer.class); kryo.register(AnotherClass.class); + kryo.register(RecordClass.class); // Make VersionFieldSerializer handle "child" field being null. VersionFieldSerializer serializer = new VersionFieldSerializer(kryo, TestClass.class); serializer.getField("child").setValueClass(TestClass.class, serializer); kryo.register(TestClass.class, serializer); - TestClass object2 = roundTrip(25, object1); + TestClass object2 = roundTrip(38, object1); assertEquals(object2.moo, object1.moo); assertEquals(object2.other.value, object1.other.value); @@ -60,6 +64,7 @@ public static class TestClass { @Since(2) public TestClass child; @Since(3) public int zzz = 123; @Since(3) public AnotherClass other; + @Since(3) public RecordClass record; public boolean equals (Object obj) { if (this == obj) return true; @@ -75,6 +80,7 @@ public boolean equals (Object obj) { if (other.text != null) return false; } else if (!text.equals(other.text)) return false; if (zzz != other.zzz) return false; + if (!Objects.equals(record, other.record)) return false; return true; } } @@ -83,6 +89,8 @@ public static class AnotherClass { @Since(1) String value; } + public record RecordClass(@Since(1) String height, int width, long x, double y) { } + private static class FutureClass { @Since(0) public Integer value; @Since(1) public FutureClass2 futureClass2; From 57b919cd2adf4c11423e11145296ea5bf061a724 Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Sun, 13 Nov 2022 18:47:02 +0100 Subject: [PATCH 06/19] Experimental FieldSerializer for Records --- .../kryo/serializers/FieldSerializer.java | 17 +++++-- .../serializers/TaggedFieldSerializer.java | 6 +-- .../serializers/VersionFieldSerializer.java | 6 +-- .../VersionedFieldSerializerTest.java | 46 ++++++++++++++++++- 4 files changed, 58 insertions(+), 17 deletions(-) diff --git a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java index 5c079f847..70916d5c1 100644 --- a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java @@ -149,11 +149,7 @@ public T read (Kryo kryo, Input input, Class type) { } if (isRecord) { - final Class[] objects = Arrays.stream(fields) - .sorted(Comparator.comparing(f -> f.index)) - .map(f -> f.field.getType()) - .toArray(Class[]::new); - object = invokeCanonicalConstructor(type, objects, values); + object = invokeCanonicalConstructor(type, fields, values); kryo.reference(object); } @@ -161,6 +157,17 @@ public T read (Kryo kryo, Input input, Class type) { return object; } + /** Invokes the canonical constructor of a record class with the given argument values. */ + static T invokeCanonicalConstructor(Class type, CachedField[] fields, Object[] values) { + T object; + final Class[] objects = Arrays.stream(fields) + .sorted(Comparator.comparing(f -> f.index)) + .map(f -> f.field.getType()) + .toArray(Class[]::new); + object = invokeCanonicalConstructor(type, objects, values); + return object; + } + /** Invokes the canonical constructor of a record class with the given argument values. */ static T invokeCanonicalConstructor (Class recordType, Class[] paramTypes, diff --git a/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java index dabeaec44..3b9e3e445 100644 --- a/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java @@ -248,11 +248,7 @@ public T read (Kryo kryo, Input input, Class type) { } if (isRecord) { - final Class[] objects = readTags.values().toList().stream() - .sorted(Comparator.comparing(f -> f.index)) - .map(f -> f.field.getType()) - .toArray(Class[]::new); - object = invokeCanonicalConstructor(type, objects, values); + object = invokeCanonicalConstructor(type, readTags.values().toList().toArray(new CachedField[0]), values); kryo.reference(object); } diff --git a/src/com/esotericsoftware/kryo/serializers/VersionFieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/VersionFieldSerializer.java index 64fa02c3e..4b5cbe583 100644 --- a/src/com/esotericsoftware/kryo/serializers/VersionFieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/VersionFieldSerializer.java @@ -145,11 +145,7 @@ public T read (Kryo kryo, Input input, Class type) { } if (isRecord) { - final Class[] objects = Arrays.stream(fields) - .sorted(Comparator.comparing(f -> f.index)) - .map(f -> f.field.getType()) - .toArray(Class[]::new); - object = invokeCanonicalConstructor(type, objects, values); + object = invokeCanonicalConstructor(type, fields, values); kryo.reference(object); } diff --git a/test/com/esotericsoftware/kryo/serializers/VersionedFieldSerializerTest.java b/test/com/esotericsoftware/kryo/serializers/VersionedFieldSerializerTest.java index 68b862bcc..46d4bd797 100644 --- a/test/com/esotericsoftware/kryo/serializers/VersionedFieldSerializerTest.java +++ b/test/com/esotericsoftware/kryo/serializers/VersionedFieldSerializerTest.java @@ -22,6 +22,8 @@ import static org.junit.jupiter.api.Assertions.*; import com.esotericsoftware.kryo.KryoTestCase; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; import com.esotericsoftware.kryo.serializers.VersionFieldSerializer.Since; import org.junit.jupiter.api.Test; @@ -40,7 +42,7 @@ void testVersionFieldSerializer () { object1.child = null; object1.other = new AnotherClass(); object1.other.value = "meow"; - object1.record = new RecordClass("1", 2, 3l, 4d); + object1.record = new RecordClass("1", 2, 3L, 4d); kryo.setDefaultSerializer(VersionFieldSerializer.class); kryo.register(AnotherClass.class); @@ -57,6 +59,44 @@ void testVersionFieldSerializer () { assertEquals(object2.other.value, object1.other.value); } + @Test + void testVersionedRecordNewToOld() { + final RecordClass recordClass = new RecordClass("1", 2, 3L, 4d); + + kryo.setDefaultSerializer(VersionFieldSerializer.class); + kryo.register(RecordClass.class); + kryo.register(OldRecordClass.class); + + Output output = new Output(2048, -1); + kryo.writeObject(output, recordClass); + output.close(); + + Input input = new Input(output.toBytes()); + Object deserialized = kryo.readObject(input, OldRecordClass.class); + input.close(); + + assertNotNull(deserialized); + } + + @Test + void testVersionedRecordOldToNew() { + final OldRecordClass recordClass = new OldRecordClass( 2, 3L, 4d); + + kryo.setDefaultSerializer(VersionFieldSerializer.class); + kryo.register(RecordClass.class); + kryo.register(OldRecordClass.class); + + Output output = new Output(2048, -1); + kryo.writeObject(output, recordClass); + output.close(); + + Input input = new Input(output.toBytes()); + Object deserialized = kryo.readObject(input, RecordClass.class); + input.close(); + + assertNotNull(deserialized); + } + public static class TestClass { @Since(1) public String text = "something"; @Since(1) public int moo = 120; @@ -89,7 +129,9 @@ public static class AnotherClass { @Since(1) String value; } - public record RecordClass(@Since(1) String height, int width, long x, double y) { } + public record OldRecordClass(int width, long x, double y) { } + + public record RecordClass(@Since(1) String height, int width, long x, double y) { } private static class FutureClass { @Since(0) public Integer value; From 8093552969121a77ac7cc0d9b02d3f3f8b147ccb Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Sun, 13 Nov 2022 19:21:08 +0100 Subject: [PATCH 07/19] Experimental FieldSerializer for Records --- .../kryo/serializers/CachedFields.java | 7 +-- .../CompatibleFieldSerializer.java | 21 +++++++-- .../kryo/serializers/FieldSerializer.java | 41 ++++++---------- .../kryo/serializers/ReflectField.java | 3 +- .../CompatibleFieldSerializerTest.java | 47 +++++++++++++++++++ .../kryo/serializers/FieldSerializerTest.java | 9 ++++ 6 files changed, 93 insertions(+), 35 deletions(-) diff --git a/src/com/esotericsoftware/kryo/serializers/CachedFields.java b/src/com/esotericsoftware/kryo/serializers/CachedFields.java index 9457f0a80..9181de587 100644 --- a/src/com/esotericsoftware/kryo/serializers/CachedFields.java +++ b/src/com/esotericsoftware/kryo/serializers/CachedFields.java @@ -136,8 +136,9 @@ private void addField (Field field, boolean asm, ArrayList fields, boolean isTransient = Modifier.isTransient(modifiers); if (isTransient && !config.serializeTransient && !config.copyTransient) return; + Class type = serializer.type; Class declaringClass = field.getDeclaringClass(); - GenericType genericType = new GenericType(declaringClass, serializer.type, field.getGenericType()); + GenericType genericType = new GenericType(declaringClass, type, field.getGenericType()); Class fieldClass = genericType.getType() instanceof Class ? (Class)genericType.getType() : field.getType(); int accessIndex = -1; if (asm // @@ -145,7 +146,7 @@ private void addField (Field field, boolean asm, ArrayList fields, && Modifier.isPublic(modifiers) // && Modifier.isPublic(fieldClass.getModifiers())) { try { - if (access == null) access = FieldAccess.get(serializer.type); + if (access == null) access = FieldAccess.get(type); accessIndex = ((FieldAccess)access).getIndex(field); } catch (RuntimeException | LinkageError ex) { if (DEBUG) debug("kryo", "Unable to use ReflectASM.", ex); @@ -153,7 +154,7 @@ private void addField (Field field, boolean asm, ArrayList fields, } CachedField cachedField; - if (unsafe && !serializer.getType().isRecord()) + if (unsafe && !type.isRecord()) cachedField = newUnsafeField(field, fieldClass, genericType); else if (accessIndex != -1) { cachedField = newAsmField(field, fieldClass, genericType); diff --git a/src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java index cab2817b5..2c989406a 100644 --- a/src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java @@ -114,8 +114,12 @@ public void write (Kryo kryo, Output output, T object) { public T read (Kryo kryo, Input input, Class type) { int pop = pushTypeVariables(); - T object = create(kryo, input, type); - kryo.reference(object); + T object = null; + final boolean isRecord = type.isRecord(); + if (!isRecord) { + object = create(kryo, input, type); + kryo.reference(object); + } CachedField[] fields = (CachedField[])kryo.getGraphContext().get(this); if (fields == null) fields = readFields(kryo, input); @@ -127,6 +131,7 @@ public T read (Kryo kryo, Input input, Class type) { fieldInput = inputChunked = new InputChunked(input, config.chunkSize); else fieldInput = input; + Object[] values = null; for (int i = 0, n = fields.length; i < n; i++) { CachedField cachedField = fields[i]; @@ -182,10 +187,20 @@ public T read (Kryo kryo, Input input, Class type) { } if (TRACE) log("Read", cachedField, input.position()); - cachedField.read(fieldInput, object); + if (object != null) { + cachedField.read(fieldInput, object); + } else { + if (values == null) values = new Object[cachedFields.fields.length]; + values[cachedField.index] = cachedField.read(fieldInput); + } if (chunked) inputChunked.nextChunk(); } + if (isRecord) { + object = invokeCanonicalConstructor(type, cachedFields.fields, values); + kryo.reference(object); + } + popTypeVariables(pop); return object; } diff --git a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java index 70916d5c1..7f461c7d3 100644 --- a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java @@ -41,6 +41,7 @@ import java.lang.reflect.Field; import java.util.Arrays; import java.util.Comparator; +import java.util.Objects; /** Serializes objects using direct field assignment. FieldSerializer is generic and can serialize most classes without any * configuration. All non-public fields are written and read by default, so it is important to evaluate each class that will be @@ -157,8 +158,7 @@ public T read (Kryo kryo, Input input, Class type) { return object; } - /** Invokes the canonical constructor of a record class with the given argument values. */ - static T invokeCanonicalConstructor(Class type, CachedField[] fields, Object[] values) { + static T invokeCanonicalConstructor(Class type, CachedField[] fields, Object[] values) { T object; final Class[] objects = Arrays.stream(fields) .sorted(Comparator.comparing(f -> f.index)) @@ -168,10 +168,7 @@ static T invokeCanonicalConstructor(Class type, CachedField[] f return object; } - /** Invokes the canonical constructor of a record class with the given argument values. */ - static T invokeCanonicalConstructor (Class recordType, - Class[] paramTypes, - Object[] args) { + static T invokeCanonicalConstructor (Class recordType, Class[] paramTypes, Object[] args) { try { Constructor canonicalConstructor; try { @@ -272,38 +269,28 @@ protected T createCopy (Kryo kryo, T original) { } public T copy (Kryo kryo, T original) { - T copy = null; + final T copy; + final CachedField[] copyFields = cachedFields.copyFields; final boolean isRecord = original.getClass().isRecord(); if (!isRecord) { copy = createCopy(kryo, original); kryo.reference(copy); - } - - Object[] values = null; - final CachedField[] copyFields = cachedFields.copyFields; - for (int i = 0, n = copyFields.length; i < n; i++) { - final CachedField field = copyFields[i]; - if (copy != null) { - field.copy(original, copy); - } else { - if (values == null) values = new Object[copyFields.length]; + for (int i = 0, n = copyFields.length; i < n; i++) { + copyFields[i].copy(original, copy); + } + } else { + final Object[] values = new Object[copyFields.length]; + for (int i = 0, n = copyFields.length; i < n; i++) { + final CachedField field = copyFields[i]; try { values[field.index] = field.getField().get(original); } catch (IllegalAccessException e) { - throw new KryoException(e); + throw new KryoException("Error accessing field: " + field.getName() + " (" + type.getName() + ")", e); } } - } - - if (isRecord) { - final Class[] objects = Arrays.stream(copyFields) - .sorted(Comparator.comparing(f -> f.index)) - .map(f -> f.field.getType()) - .toArray(Class[]::new); - copy = (T) invokeCanonicalConstructor(type, objects, values); + copy = (T) invokeCanonicalConstructor(type, copyFields, values); kryo.reference(copy); } - return copy; } diff --git a/src/com/esotericsoftware/kryo/serializers/ReflectField.java b/src/com/esotericsoftware/kryo/serializers/ReflectField.java index eb0a7fdb1..cd0eaf10d 100644 --- a/src/com/esotericsoftware/kryo/serializers/ReflectField.java +++ b/src/com/esotericsoftware/kryo/serializers/ReflectField.java @@ -50,7 +50,7 @@ public Object get (Object object) throws IllegalAccessException { public void set (Object object, Object value) throws IllegalAccessException { field.set(object, value); } - + public void write (Output output, Object object) { Kryo kryo = fieldSerializer.kryo; try { @@ -178,7 +178,6 @@ public Object read(Input input) { } kryo.getGenerics().popGenericType(); - return value; } catch (KryoException ex) { ex.addTrace(name + " (" + fieldSerializer.type.getName() + ")"); diff --git a/test/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializerTest.java b/test/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializerTest.java index a0f6a0b29..9e866a6ac 100644 --- a/test/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializerTest.java +++ b/test/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializerTest.java @@ -515,6 +515,49 @@ void testClassWithGenericField () { roundTrip(9, new ClassWithGenericField<>(1)); } + @Test + void testRecordNewToOld() { + final RecordClass recordClass = new RecordClass("1", 2, 3L, 4d); + + kryo.setDefaultSerializer(CompatibleFieldSerializer.class); + kryo.register(RecordClass.class); + kryo.register(OldRecordClass.class); + + Output output = new Output(2048, -1); + kryo.writeObject(output, recordClass); + output.close(); + + Input input = new Input(output.toBytes()); + OldRecordClass deserialized = kryo.readObject(input, OldRecordClass.class); + input.close(); + + assertEquals(deserialized.width(), recordClass.width()); + assertEquals(deserialized.x(), recordClass.x()); + assertEquals(deserialized.y(), recordClass.y()); + } + + @Test + void testRecordOldToNew() { + final OldRecordClass recordClass = new OldRecordClass(3L, 4d, 2); + + kryo.setDefaultSerializer(CompatibleFieldSerializer.class); + kryo.register(RecordClass.class); + kryo.register(OldRecordClass.class); + + Output output = new Output(2048, -1); + kryo.writeObject(output, recordClass); + output.close(); + + Input input = new Input(output.toBytes()); + RecordClass deserialized = kryo.readObject(input, RecordClass.class); + input.close(); + + assertNull(deserialized.height()); + assertEquals(deserialized.width(), recordClass.width()); + assertEquals(deserialized.x(), recordClass.x()); + assertEquals(deserialized.y(), recordClass.y()); + } + public static class TestClass { public String text = "something"; public int moo = 120; @@ -811,4 +854,8 @@ public boolean equals (Object o) { return Objects.equals(value, that.value); } } + + public record OldRecordClass(long x, double y, int width) { } + + public record RecordClass(String height, int width, long x, double y) { } } diff --git a/test/com/esotericsoftware/kryo/serializers/FieldSerializerTest.java b/test/com/esotericsoftware/kryo/serializers/FieldSerializerTest.java index 1a749e5d9..aedaceff8 100644 --- a/test/com/esotericsoftware/kryo/serializers/FieldSerializerTest.java +++ b/test/com/esotericsoftware/kryo/serializers/FieldSerializerTest.java @@ -715,6 +715,13 @@ void testCircularReference () { fail("Exception was expected"); } + @Test + void testRecord() { + kryo.register(RecordClass.class); + + roundTrip(13, new RecordClass("1", 1, 1L, 1d)); + } + public static class DefaultTypes { // Primitives. public boolean booleanField; @@ -1299,4 +1306,6 @@ public Inner(CircularReference a) { } } + public record RecordClass(String height, int width, long x, double y) { } + } From 86eceb8fcef15541a49e30aecf3888b332b3c627 Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Fri, 16 Dec 2022 12:56:47 +0100 Subject: [PATCH 08/19] Adjust expected default serializer count --- test/com/esotericsoftware/kryo/SerializationCompatTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/com/esotericsoftware/kryo/SerializationCompatTest.java b/test/com/esotericsoftware/kryo/SerializationCompatTest.java index 63ff89953..9fdc86481 100644 --- a/test/com/esotericsoftware/kryo/SerializationCompatTest.java +++ b/test/com/esotericsoftware/kryo/SerializationCompatTest.java @@ -84,7 +84,7 @@ class SerializationCompatTest extends KryoTestCase { } } private static final int EXPECTED_DEFAULT_SERIALIZER_COUNT = JAVA_VERSION < 11 - ? 58 : JAVA_VERSION < 14 ? 68 : 69; // Also change Kryo#defaultSerializers. + ? 58 : 68; // Also change Kryo#defaultSerializers. private static final List TEST_DATAS = new ArrayList<>(); static { From 5e1d9215a2466fb6d8121d5e37ee7f9eb1f8b66b Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Fri, 16 Dec 2022 13:14:09 +0100 Subject: [PATCH 09/19] Adjust GitHub workflow to use JDK17 --- .github/workflows/pr-workflow.yml | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/.github/workflows/pr-workflow.yml b/.github/workflows/pr-workflow.yml index ec8586d72..07b14755e 100644 --- a/.github/workflows/pr-workflow.yml +++ b/.github/workflows/pr-workflow.yml @@ -11,26 +11,12 @@ jobs: - uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: 11 + java-version: 17 cache: 'maven' - - name: Build with JDK 11 + - name: Build with JDK 17 run: mvn -B install --no-transfer-progress -DskipTests - - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: 8 - - name: Test with JDK 8 - run: mvn -v && mvn -B test - - - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: 11 - - name: Test with JDK 11 - run: mvn -v && mvn -B test - - uses: actions/setup-java@v3 with: distribution: 'temurin' From ca3fb981eff62c42d7f1fdc1318d3d3595479021 Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Fri, 16 Dec 2022 18:29:22 +0100 Subject: [PATCH 10/19] Cleanup --- .../kryo/serializers/RecordSerializerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java b/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java index 89bfd2dd1..61142d28b 100644 --- a/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java +++ b/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java @@ -45,7 +45,7 @@ public record RecordRectangle (String height, int width, long x, double y) { } @Test public void testBasicRecord() { - kryo.register(RecordRectangle.class, new FieldSerializer<>(kryo, RecordRectangle.class)); + kryo.register(RecordRectangle.class); final var r1 = new RecordRectangle("one", 2, 3L, 4.0); final var output = new Output(64); @@ -112,7 +112,7 @@ public RecordWithConstructor(String height) { @Test public void testRecordWithConstructor() { - kryo.register(RecordWithConstructor.class, new FieldSerializer<>(kryo, RecordWithConstructor.class)); + kryo.register(RecordWithConstructor.class); final var r1 = new RecordWithConstructor("ten"); final var output = new Output(32); From c41eb11a801dddcd8445f6a22b2c02c8119efa58 Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Fri, 16 Dec 2022 18:29:39 +0100 Subject: [PATCH 11/19] Require JDK 17 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fbeccde61..383815c78 100644 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ ${basedir} 5 - 1.8 + 17 UTF-8 5.9.1 From 78986e0139927ce8afa8d17736148c6d701e6201 Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Fri, 16 Dec 2022 18:46:58 +0100 Subject: [PATCH 12/19] Remove registration for `RecordSerializer` --- src/com/esotericsoftware/kryo/Kryo.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/com/esotericsoftware/kryo/Kryo.java b/src/com/esotericsoftware/kryo/Kryo.java index 0701b8572..17e6122b2 100644 --- a/src/com/esotericsoftware/kryo/Kryo.java +++ b/src/com/esotericsoftware/kryo/Kryo.java @@ -229,10 +229,6 @@ public Kryo (ClassResolver classResolver, ReferenceResolver referenceResolver) { OptionalSerializers.addDefaultSerializers(this); TimeSerializers.addDefaultSerializers(this); ImmutableCollectionsSerializers.addDefaultSerializers(this); - // Add RecordSerializer if JDK 14+ available - if (isClassAvailable("java.lang.Record")) { - //addDefaultSerializer("java.lang.Record", RecordSerializer.class); - } lowPriorityDefaultSerializerCount = defaultSerializers.size(); // Primitives and string. Primitive wrappers automatically use the same registration as primitives. @@ -283,17 +279,6 @@ public void addDefaultSerializer (Class type, SerializerFactory serializerFactor insertDefaultSerializer(type, serializerFactory); } - /** Instances with the specified class name will use the specified serializer when {@link #register(Class)} or - * {@link #register(Class, int)} are called. - * @see #setDefaultSerializer(Class) */ - private void addDefaultSerializer (String className, Class serializer) { - try { - addDefaultSerializer(Class.forName(className), serializer); - } catch (ClassNotFoundException e) { - throw new KryoException("default serializer cannot be added: " + className); - } - } - /** Instances of the specified class will use the specified serializer when {@link #register(Class)} or * {@link #register(Class, int)} are called. Serializer instances are created as needed via * {@link ReflectionSerializerFactory#newSerializer(Kryo, Class, Class)}. By default, the following classes have a default From b89a76488ff2ceef8bd63e03a33d67b3bf2f1990 Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Fri, 16 Dec 2022 18:49:30 +0100 Subject: [PATCH 13/19] Cleanup --- src/com/esotericsoftware/kryo/serializers/CachedFields.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/esotericsoftware/kryo/serializers/CachedFields.java b/src/com/esotericsoftware/kryo/serializers/CachedFields.java index 9181de587..679749b98 100644 --- a/src/com/esotericsoftware/kryo/serializers/CachedFields.java +++ b/src/com/esotericsoftware/kryo/serializers/CachedFields.java @@ -185,7 +185,7 @@ else if (accessIndex != -1) { "Cached " + fieldClass.getSimpleName() + " field: " + field.getName() + " (" + className(declaringClass) + ")"); } - final RecordComponent[] recordComponents = serializer.getType().getRecordComponents(); + final RecordComponent[] recordComponents = type.getRecordComponents(); if (recordComponents != null) { for (int i = 0; i < recordComponents.length; i++) { RecordComponent recordComponent = recordComponents[i]; From 62805b496ea77e6f1ef29b1251195d551c78db44 Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Fri, 16 Dec 2022 18:54:55 +0100 Subject: [PATCH 14/19] Cleanup --- .../kryo/serializers/FieldSerializer.java | 12 +++++------- .../kryo/serializers/RecordSerializerTest.java | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java index 7f461c7d3..39ab242d5 100644 --- a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java @@ -159,28 +159,26 @@ public T read (Kryo kryo, Input input, Class type) { } static T invokeCanonicalConstructor(Class type, CachedField[] fields, Object[] values) { - T object; final Class[] objects = Arrays.stream(fields) .sorted(Comparator.comparing(f -> f.index)) .map(f -> f.field.getType()) .toArray(Class[]::new); - object = invokeCanonicalConstructor(type, objects, values); - return object; + return invokeCanonicalConstructor(type, objects, values); } - static T invokeCanonicalConstructor (Class recordType, Class[] paramTypes, Object[] args) { + static T invokeCanonicalConstructor (Class type, Class[] paramTypes, Object[] args) { try { Constructor canonicalConstructor; try { - canonicalConstructor = recordType.getConstructor(paramTypes); + canonicalConstructor = type.getConstructor(paramTypes); } catch (NoSuchMethodException e) { - canonicalConstructor = recordType.getDeclaredConstructor(paramTypes); + canonicalConstructor = type.getDeclaredConstructor(paramTypes); canonicalConstructor.setAccessible(true); } return canonicalConstructor.newInstance(args); } catch (Throwable t) { KryoException ex = new KryoException(t); - ex.addTrace("Could not construct type (" + recordType.getName() + ")"); + ex.addTrace("Could not construct type (" + type.getName() + ")"); throw ex; } } diff --git a/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java b/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java index 61142d28b..24a3fd74c 100644 --- a/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java +++ b/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java @@ -48,7 +48,7 @@ public void testBasicRecord() { kryo.register(RecordRectangle.class); final var r1 = new RecordRectangle("one", 2, 3L, 4.0); - final var output = new Output(64); + final var output = new Output(32); kryo.writeObject(output, r1); final var input = new Input(output.getBuffer(), 0, output.position()); final var r2 = kryo.readObject(input, RecordRectangle.class); From 2ecda65a6f4d2d5950297598be931d2db6c674de Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Fri, 16 Dec 2022 21:31:59 +0100 Subject: [PATCH 15/19] Cleanup --- .../kryo/serializers/FieldSerializer.java | 5 ++++- .../kryo/serializers/FieldSerializerTest.java | 13 +++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java index 39ab242d5..f3faf2744 100644 --- a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java @@ -281,7 +281,7 @@ public T copy (Kryo kryo, T original) { for (int i = 0, n = copyFields.length; i < n; i++) { final CachedField field = copyFields[i]; try { - values[field.index] = field.getField().get(original); + values[field.index] = field.get(original); } catch (IllegalAccessException e) { throw new KryoException("Error accessing field: " + field.getName() + " (" + type.getName() + ")", e); } @@ -413,6 +413,9 @@ public String toString () { public abstract void copy (Object original, Object copy); + Object get(Object object) throws IllegalAccessException { + return field.get(object); + } } /** Indicates a field should be ignored when its declaring class is registered unless the {@link Kryo#getContext() context} has diff --git a/test/com/esotericsoftware/kryo/serializers/FieldSerializerTest.java b/test/com/esotericsoftware/kryo/serializers/FieldSerializerTest.java index 133c40cf0..a3cc03c52 100644 --- a/test/com/esotericsoftware/kryo/serializers/FieldSerializerTest.java +++ b/test/com/esotericsoftware/kryo/serializers/FieldSerializerTest.java @@ -717,12 +717,21 @@ void testCircularReference () { } @Test - void testRecord() { + void testRecord () { kryo.register(RecordClass.class); - + roundTrip(13, new RecordClass("1", 1, 1L, 1d)); } + @Test + void testCopyRecord () { + kryo.register(RecordClass.class); + + final RecordClass o = new RecordClass("1", 1, 1L, 1d); + final RecordClass copy = kryo.copy(o); + doAssertEquals(o, copy); + } + public static class DefaultTypes { // Primitives. public boolean booleanField; From 77bd1507e735650808ae96a0254997b426700d62 Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Fri, 16 Dec 2022 21:33:35 +0100 Subject: [PATCH 16/19] Cleanup --- .../kryo/serializers/RecordSerializerTest.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java b/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java index 24a3fd74c..7dacde9f5 100644 --- a/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java +++ b/test-jdk14/com/esotericsoftware/kryo/serializers/RecordSerializerTest.java @@ -306,8 +306,7 @@ public static record RecordWithSuperType(Number n) {} @Test void testRecordWithSuperType() { - var rc = new RecordSerializer(); - kryo.register(RecordWithSuperType.class, rc); + kryo.register(RecordWithSuperType.class); final var r = new RecordWithSuperType(1L); final var output = new Output(32); @@ -329,4 +328,13 @@ void testNonPublicRecords() { roundTrip(4, new PackagePrivateRecord(1, "s1")); roundTrip(4, new PrivateRecord("s2",2)); } + + record InterfaceRecord(int i, CharSequence s) {} + + @Test + void testRecordWithInterface() { + kryo.register(InterfaceRecord.class); + + roundTrip(5, new InterfaceRecord(1, "s1")); + } } From 01d2026aeded2cfdc8045b4a023cc07771455a13 Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Sat, 17 Dec 2022 16:59:25 +0100 Subject: [PATCH 17/19] Add tests for `TaggedFieldSerializer` --- .../serializers/TaggedFieldSerializer.java | 7 +-- .../TaggedFieldSerializerTest.java | 45 ++++++++++++++++++- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java index 3b9e3e445..4da130473 100644 --- a/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java @@ -190,7 +190,8 @@ public T read (Kryo kryo, Input input, Class type) { fieldInput = inputChunked = new InputChunked(input, config.chunkSize); else fieldInput = input; - IntMap readTags = this.readTags; + + CachedField[] fields = cachedFields.fields; Object[] values = null; for (int i = 0; i < fieldCount; i++) { int tag = input.readVarInt(true); @@ -241,14 +242,14 @@ public T read (Kryo kryo, Input input, Class type) { if (object != null) { cachedField.read(fieldInput, object); } else { - if (values == null) values = new Object[fieldCount]; + if (values == null) values = new Object[fields.length]; values[cachedField.index] = cachedField.read(fieldInput); } if (chunked) inputChunked.nextChunk(); } if (isRecord) { - object = invokeCanonicalConstructor(type, readTags.values().toList().toArray(new CachedField[0]), values); + object = invokeCanonicalConstructor(type, fields, values); kryo.reference(object); } diff --git a/test/com/esotericsoftware/kryo/serializers/TaggedFieldSerializerTest.java b/test/com/esotericsoftware/kryo/serializers/TaggedFieldSerializerTest.java index f2a57789b..d0a786e28 100644 --- a/test/com/esotericsoftware/kryo/serializers/TaggedFieldSerializerTest.java +++ b/test/com/esotericsoftware/kryo/serializers/TaggedFieldSerializerTest.java @@ -141,6 +141,47 @@ void testForwardCompatibility () { assertEquals(futureArray[1], presentArray[1]); } + @Test + void testTaggedRecordNewToOld() { + final RecordClass recordClass = new RecordClass("1", 2, 3L, 4d); + + final TaggedFieldSerializer.TaggedFieldSerializerConfig cfg = new TaggedFieldSerializer.TaggedFieldSerializerConfig(); + cfg.setChunkedEncoding(true); + cfg.setReadUnknownTagData(true); + kryo.setDefaultSerializer(new TaggedFieldSerializerFactory(cfg)); + kryo.register(RecordClass.class); + kryo.register(OldRecordClass.class); + + Output output = new Output(2048, -1); + kryo.writeObject(output, recordClass); + output.close(); + + Input input = new Input(output.toBytes()); + Object deserialized = kryo.readObject(input, OldRecordClass.class); + input.close(); + + assertNotNull(deserialized); + } + + @Test + void testTaggedRecordOldToNew() { + final OldRecordClass recordClass = new OldRecordClass(3L, 4d, 2); + + kryo.setDefaultSerializer(TaggedFieldSerializer.class); + kryo.register(RecordClass.class); + kryo.register(OldRecordClass.class); + + Output output = new Output(2048, -1); + kryo.writeObject(output, recordClass); + output.close(); + + Input input = new Input(output.toBytes()); + Object deserialized = kryo.readObject(input, RecordClass.class); + input.close(); + + assertNotNull(deserialized); + } + /** Attempts to register a class with a field tagged with a value already used in its superclass. Should receive * IllegalArgumentException. */ @Test @@ -230,7 +271,9 @@ public static class AnotherClass { @Tag(1) String value; } - public record RecordClass(@Tag(0) String height, @Tag(1) int width, @Tag(2) long x, @Tag(3) double y) { } + public record OldRecordClass(@Tag(0) long x, @Tag(1) double y, @Tag(2) int width) { } + + public record RecordClass(@Tag(3) String height, @Tag(2) int width, @Tag(0) long x, @Tag(1) double y) { } private static class FutureClass { @Tag(0) public Integer value; From 0ca27e86fc0fe95ece00b87b19745c34fac398e3 Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Sat, 17 Dec 2022 17:13:47 +0100 Subject: [PATCH 18/19] Cleanup --- src/com/esotericsoftware/kryo/serializers/CachedFields.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/com/esotericsoftware/kryo/serializers/CachedFields.java b/src/com/esotericsoftware/kryo/serializers/CachedFields.java index 679749b98..a081738fb 100644 --- a/src/com/esotericsoftware/kryo/serializers/CachedFields.java +++ b/src/com/esotericsoftware/kryo/serializers/CachedFields.java @@ -191,6 +191,7 @@ else if (accessIndex != -1) { RecordComponent recordComponent = recordComponents[i]; if (recordComponent.getName().equals(field.getName())) { cachedField.index = i; + break; } } } From 9ff145b2430453fdf7cce47e55f49724e60c673a Mon Sep 17 00:00:00 2001 From: Thomas Heigl Date: Tue, 20 Dec 2022 20:59:21 +0100 Subject: [PATCH 19/19] Remove unnecessary call to `kry.reference()` since records cannot contain themselves --- .../kryo/serializers/CompatibleFieldSerializer.java | 1 - src/com/esotericsoftware/kryo/serializers/FieldSerializer.java | 2 -- .../kryo/serializers/TaggedFieldSerializer.java | 1 - .../kryo/serializers/VersionFieldSerializer.java | 1 - 4 files changed, 5 deletions(-) diff --git a/src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java index 2c989406a..44a0d7aff 100644 --- a/src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java @@ -198,7 +198,6 @@ public T read (Kryo kryo, Input input, Class type) { if (isRecord) { object = invokeCanonicalConstructor(type, cachedFields.fields, values); - kryo.reference(object); } popTypeVariables(pop); diff --git a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java index f3faf2744..b499de1a6 100644 --- a/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/FieldSerializer.java @@ -151,7 +151,6 @@ public T read (Kryo kryo, Input input, Class type) { if (isRecord) { object = invokeCanonicalConstructor(type, fields, values); - kryo.reference(object); } popTypeVariables(pop); @@ -287,7 +286,6 @@ public T copy (Kryo kryo, T original) { } } copy = (T) invokeCanonicalConstructor(type, copyFields, values); - kryo.reference(copy); } return copy; } diff --git a/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java index 4da130473..b28c80a38 100644 --- a/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/TaggedFieldSerializer.java @@ -250,7 +250,6 @@ public T read (Kryo kryo, Input input, Class type) { if (isRecord) { object = invokeCanonicalConstructor(type, fields, values); - kryo.reference(object); } popTypeVariables(pop); diff --git a/src/com/esotericsoftware/kryo/serializers/VersionFieldSerializer.java b/src/com/esotericsoftware/kryo/serializers/VersionFieldSerializer.java index 4b5cbe583..8aa73ddbc 100644 --- a/src/com/esotericsoftware/kryo/serializers/VersionFieldSerializer.java +++ b/src/com/esotericsoftware/kryo/serializers/VersionFieldSerializer.java @@ -146,7 +146,6 @@ public T read (Kryo kryo, Input input, Class type) { if (isRecord) { object = invokeCanonicalConstructor(type, fields, values); - kryo.reference(object); } popTypeVariables(pop);