Skip to content

Commit 5a7b7c5

Browse files
committed
Support for same named fields in class hierarchy
Added an option/strategy to influence how CachedFields are recognized in FieldSerieliazer and CompatibleFieldSerializer. It is an improvement for FieldSerializer (influences getField/removeField methods) and nearly a must have in CompatibleFieldSerialzer in case your class hierarchy contains same named fields (e.g. class A has a field named "a", and class B extends A and also contains a field named "a"). Per default, both mentioned serializers behave like before (DEFAULT strategy used). To change it, use `kryo.getFieldSerializerConfig().setCachedFieldNameStrategy( FieldSerializer.CachedFieldNameStrategy.EXTENDED);`. At this moment, class simple name is added to CachedField name. This helps in CompatibleFieldSerializer write the right value to right class field. Without EXTENDED, all same named values are written just to the first found field. Resolves: #424 See also: #187
1 parent 132456e commit 5a7b7c5

File tree

8 files changed

+255
-15
lines changed

8 files changed

+255
-15
lines changed

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ A serialization library needs special knowledge on how to create new instances,
479479

480480
The Serializer class has a `copy` method that does the work. These methods can be ignored when implementing application specific serializers if the copying functionality will not be used. All serializers provided with Kryo support copying. Multiple references to the same object and circular references are handled by the framework automatically.
481481

482-
Similar to the `read()` Serializer method, `kryo.reference()` must be called before Kryo can be used to copy child objects. See [Serializers](#Serializers) for more information.
482+
Similar to the `read()` Serializer method, `kryo.reference()` must be called before Kryo can be used to copy child objects. See [Serializers](#serializers) for more information.
483483

484484
Similar to KryoSerializable, classes can implement KryoCopyable to do their own copying:
485485

@@ -561,7 +561,20 @@ VersionFieldSerializer extends FieldSerializer and allows fields to have a `@Sin
561561

562562
TaggedFieldSerializer extends FieldSerializer to only serialize fields that have a `@Tag(int)` annotation, providing backward compatibility so new fields can be added. And it also provides forward compatibility by `setIgnoreUnknownTags(true)`, thus any unknown field tags will be ignored. TaggedFieldSerializer has two advantages over VersionFieldSerializer: 1) fields can be renamed and 2) fields marked with the `@Deprecated` annotation will be ignored when reading old bytes and won't be written to new bytes. Deprecation effectively removes the field from serialization, though the field and `@Tag` annotation must remain in the class. Deprecated fields can optionally be made private and/or renamed so they don't clutter the class (eg, `ignored`, `ignored2`). For these reasons, TaggedFieldSerializer generally provides more flexibility for classes to evolve. The downside is that it has a small amount of additional overhead compared to VersionFieldSerializer (an additional varint per field).
563563

564-
CompatibleFieldSerializer extends FieldSerializer to provide both forward and backward compatibility, meaning fields can be added or removed without invalidating previously serialized bytes. Changing the type of a field is not supported. Like FieldSerializer, it can serialize most classes without needing annotations. The forward and backward compatibility comes at a cost: the first time the class is encountered in the serialized bytes, a simple schema is written containing the field name strings. Also, during serialization and deserialization buffers are allocated to perform chunked encoding. This is what enables CompatibleFieldSerializer to skip bytes for fields it does not know about. When Kryo is configured to use references, there can be a [problem](https://github.com/EsotericSoftware/kryo/issues/286#issuecomment-74870545) with CompatibleFieldSerializer if a field is removed.
564+
CompatibleFieldSerializer extends FieldSerializer to provide both forward and backward compatibility, meaning fields can be added or removed without invalidating previously serialized bytes. Changing the type of a field is not supported. Like FieldSerializer, it can serialize most classes without needing annotations. The forward and backward compatibility comes at a cost: the first time the class is encountered in the serialized bytes, a simple schema is written containing the field name strings. Also, during serialization and deserialization buffers are allocated to perform chunked encoding. This is what enables CompatibleFieldSerializer to skip bytes for fields it does not know about. When Kryo is configured to use references, there can be a [problem](https://github.com/EsotericSoftware/kryo/issues/286#issuecomment-74870545) with CompatibleFieldSerializer if a field is removed. In case your class inheritance hierarchy contains same named fields, use the `CachedFieldNameStrategy.EXTENDED` strategy.
565+
566+
```java
567+
class A {
568+
String a;
569+
}
570+
571+
class B extends A {
572+
String a;
573+
}
574+
...
575+
// use `EXTENDED` name strategy, otherwise serialized object can't be deserialized correctly. Attention, `EXTENDED` strategy increases the serialized footprint.
576+
kryo.getFieldSerializerConfig().setCachedFieldNameStrategy(FieldSerializer.CachedFieldNameStrategy.EXTENDED);
577+
```
565578

566579
Additional serializers can easily be developed for forward and backward compatibility, such as a serializer that uses an external, hand written schema.
567580

src/com/esotericsoftware/kryo/Kryo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ public Registration register (Registration registration) {
462462
Registration existing = getRegistration(registration.getId());
463463
if (DEBUG && existing != null && existing.getType() != registration.getType()) {
464464
debug("An existing registration with a different type already uses ID: " + registration.getId()
465-
+ "\nExisting registration: " + existing + "\nUnable to set registration: " + registration);
465+
+ "\nExisting registration: " + existing + "\nis now overwritten with: " + registration);
466466
}
467467

468468
return classResolver.register(registration);

src/com/esotericsoftware/kryo/serializers/CompatibleFieldSerializer.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public void write (Kryo kryo, Output output, T object) {
5858
if (TRACE) trace("kryo", "Write " + fields.length + " field names.");
5959
output.writeVarInt(fields.length, true);
6060
for (int i = 0, n = fields.length; i < n; i++)
61-
output.writeString(fields[i].field.getName());
61+
output.writeString(getCachedFieldName(fields[i]));
6262
}
6363

6464
OutputChunked outputChunked = new OutputChunked(output, 1024);
@@ -88,7 +88,7 @@ public T read (Kryo kryo, Input input, Class<T> type) {
8888
for (int i = 0; i < length; i++) {
8989
String schemaName = names[i];
9090
for (int ii = 0, nn = allFields.length; ii < nn; ii++) {
91-
if (allFields[ii].field.getName().equals(schemaName)) {
91+
if (getCachedFieldName(allFields[ii]).equals(schemaName)) {
9292
fields[i] = allFields[ii];
9393
continue outer;
9494
}
@@ -108,7 +108,7 @@ public T read (Kryo kryo, Input input, Class<T> type) {
108108

109109
while (low <= high) {
110110
mid = (low + high) >>> 1;
111-
String midVal = allFields[mid].field.getName();
111+
String midVal = getCachedFieldName(allFields[mid]);
112112
compare = schemaName.compareTo(midVal);
113113

114114
if (compare < 0) {
@@ -137,7 +137,7 @@ else if (compare > 0) {
137137
// Generic type used to instantiate this field could have
138138
// been changed in the meantime. Therefore take the most
139139
// up-to-date definition of a field
140-
cachedField = getField(cachedField.field.getName());
140+
cachedField = getField(getCachedFieldName(cachedField));
141141
}
142142
if (cachedField == null) {
143143
if (TRACE) trace("kryo", "Skip obsolete field.");

src/com/esotericsoftware/kryo/serializers/FieldSerializer.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ private CachedFieldFactory getUnsafeFieldFactory () {
410410

411411
public int compare (CachedField o1, CachedField o2) {
412412
// Fields are sorted by alpha so the order of the data is known.
413-
return o1.field.getName().compareTo(o2.field.getName());
413+
return getCachedFieldName(o1).compareTo(getCachedFieldName(o2));
414414
}
415415

416416
/** Sets the default value for {@link CachedField#setCanBeNull(boolean)}. Calling this method resets the {@link #getFields()
@@ -542,15 +542,19 @@ protected T create (Kryo kryo, Input input, Class<T> type) {
542542
/** Allows specific fields to be optimized. */
543543
public CachedField getField (String fieldName) {
544544
for (CachedField cachedField : fields)
545-
if (cachedField.field.getName().equals(fieldName)) return cachedField;
545+
if (getCachedFieldName(cachedField).equals(fieldName)) return cachedField;
546546
throw new IllegalArgumentException("Field \"" + fieldName + "\" not found on class: " + type.getName());
547547
}
548548

549+
protected String getCachedFieldName(CachedField cachedField) {
550+
return config.getCachedFieldNameStrategy().getName(cachedField);
551+
}
552+
549553
/** Removes a field so that it won't be serialized. */
550554
public void removeField (String fieldName) {
551555
for (int i = 0; i < fields.length; i++) {
552556
CachedField cachedField = fields[i];
553-
if (cachedField.field.getName().equals(fieldName)) {
557+
if (getCachedFieldName(cachedField).equals(fieldName)) {
554558
CachedField[] newFields = new CachedField[fields.length - 1];
555559
System.arraycopy(fields, 0, newFields, 0, i);
556560
System.arraycopy(fields, i + 1, newFields, i, newFields.length - i);
@@ -562,7 +566,7 @@ public void removeField (String fieldName) {
562566

563567
for (int i = 0; i < transientFields.length; i++) {
564568
CachedField cachedField = transientFields[i];
565-
if (cachedField.field.getName().equals(fieldName)) {
569+
if (getCachedFieldName(cachedField).equals(fieldName)) {
566570
CachedField[] newFields = new CachedField[transientFields.length - 1];
567571
System.arraycopy(transientFields, 0, newFields, 0, i);
568572
System.arraycopy(transientFields, i + 1, newFields, i, newFields.length - i);
@@ -721,6 +725,25 @@ public static interface CachedFieldFactory {
721725
public CachedField createCachedField (Class fieldClass, Field field, FieldSerializer ser);
722726
}
723727

728+
public interface CachedFieldNameStrategy {
729+
730+
CachedFieldNameStrategy DEFAULT = new CachedFieldNameStrategy() {
731+
@Override
732+
public String getName(CachedField cachedField) {
733+
return cachedField.field.getName();
734+
}
735+
};
736+
737+
CachedFieldNameStrategy EXTENDED = new CachedFieldNameStrategy() {
738+
@Override
739+
public String getName(CachedField cachedField) {
740+
return cachedField.field.getDeclaringClass().getSimpleName() + "." + cachedField.field.getName();
741+
}
742+
};
743+
744+
String getName(CachedField cachedField);
745+
}
746+
724747
/** Indicates a field should be ignored when its declaring class is registered unless the {@link Kryo#getContext() context} has
725748
* a value set for the specified key. This can be useful when a field must be serialized for one purpose, but not for another.
726749
* Eg, a class for a networked application could have a field that should not be serialized and sent to clients, but should be

src/com/esotericsoftware/kryo/serializers/FieldSerializerConfig.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public class FieldSerializerConfig implements Cloneable {
3838
/** If set, transient fields will be serialized */
3939
private boolean serializeTransient = false;
4040

41+
private FieldSerializer.CachedFieldNameStrategy cachedFieldNameStrategy = FieldSerializer.CachedFieldNameStrategy.DEFAULT;
42+
4143
{
4244
useAsm = !FieldSerializer.unsafeAvailable;
4345
if (TRACE) trace("kryo.FieldSerializerConfig", "useAsm: " + useAsm);
@@ -136,4 +138,13 @@ public boolean isCopyTransient() {
136138
public boolean isSerializeTransient() {
137139
return serializeTransient;
138140
}
141+
142+
public FieldSerializer.CachedFieldNameStrategy getCachedFieldNameStrategy() {
143+
return cachedFieldNameStrategy;
144+
}
145+
146+
public void setCachedFieldNameStrategy(FieldSerializer.CachedFieldNameStrategy cachedFieldNameStrategy) {
147+
this.cachedFieldNameStrategy = cachedFieldNameStrategy;
148+
if (TRACE) trace("kryo.FieldSerializerConfig", "CachedFieldNameStrategy: " + cachedFieldNameStrategy);
149+
}
139150
}

test/com/esotericsoftware/kryo/CompatibleFieldSerializerTest.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@
1515
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
1616
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
1717
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
18-
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
19-
18+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
19+
2020
package com.esotericsoftware.kryo;
2121

2222
import java.io.FileNotFoundException;
2323

2424
import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer;
25+
import com.esotericsoftware.kryo.serializers.FieldSerializer;
2526

2627
/** @author Nathan Sweet <[email protected]> */
2728
public class CompatibleFieldSerializerTest extends KryoTestCase {
@@ -71,6 +72,21 @@ public void testRemovedField () throws FileNotFoundException {
7172
assertEquals(object1, object2);
7273
}
7374

75+
public void testExtendedClass() throws FileNotFoundException {
76+
ExtendedTestClass extendedObject = new ExtendedTestClass();
77+
78+
// this test would fail with DEFAULT field name strategy
79+
kryo.getFieldSerializerConfig().setCachedFieldNameStrategy(FieldSerializer.CachedFieldNameStrategy.EXTENDED);
80+
81+
CompatibleFieldSerializer serializer = new CompatibleFieldSerializer(kryo, ExtendedTestClass.class);
82+
kryo.register(ExtendedTestClass.class, serializer);
83+
roundTrip(286, 286, extendedObject);
84+
85+
ExtendedTestClass object2 = (ExtendedTestClass) kryo.readClassAndObject(input);
86+
assertEquals(extendedObject, object2);
87+
}
88+
89+
7490
static public class TestClass {
7591
public String text = "something";
7692
public int moo = 120;
@@ -97,7 +113,36 @@ public boolean equals (Object obj) {
97113
}
98114
}
99115

116+
static public class ExtendedTestClass extends TestClass {
117+
// keep the same names of attributes like TestClass
118+
public String text = "extendedSomething";
119+
public int moo = 127;
120+
public long moo2 = 5555;
121+
public TestClass child;
122+
public int zzz = 222;
123+
public AnotherClass other;
124+
125+
public boolean equals (Object obj) {
126+
if (this == obj) return true;
127+
if (obj == null) return false;
128+
if (getClass() != obj.getClass()) return false;
129+
ExtendedTestClass other = (ExtendedTestClass) obj;
130+
131+
if (!super.equals(obj)) return false;
132+
if (child == null) {
133+
if (other.child != null) return false;
134+
} else if (!child.equals(other.child)) return false;
135+
if (moo != other.moo) return false;
136+
if (moo2 != other.moo2) return false;
137+
if (text == null) {
138+
if (other.text != null) return false;
139+
} else if (!text.equals(other.text)) return false;
140+
if (zzz != other.zzz) return false;
141+
return true;
142+
}
143+
}
144+
100145
static public class AnotherClass {
101146
String value;
102147
}
103-
}
148+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/* Copyright (c) 2008, Nathan Sweet
2+
* All rights reserved.
3+
*
4+
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following
5+
* conditions are met:
6+
*
7+
* - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8+
* - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
9+
* disclaimer in the documentation and/or other materials provided with the distribution.
10+
* - Neither the name of Esoteric Software nor the names of its contributors may be used to endorse or promote products derived
11+
* from this software without specific prior written permission.
12+
*
13+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
14+
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
15+
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
16+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
17+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
18+
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
19+
20+
package com.esotericsoftware.kryo;
21+
22+
import com.esotericsoftware.kryo.serializers.FieldSerializer;
23+
import org.junit.Assert;
24+
25+
/**
26+
* Created by phamrak on 8.6.2016.
27+
*/
28+
public class FieldSerializerInheritanceTest extends KryoTestCase {
29+
public void testDefaultStrategyForDefaultClass() {
30+
TestDefault testDefault = new TestDefault();
31+
testDefault.a = "someDefaultValue";
32+
kryo.setDefaultSerializer(FieldSerializer.class);
33+
kryo.register(TestDefault.class);
34+
35+
roundTrip(17, 17, testDefault);
36+
37+
FieldSerializer serializer = (FieldSerializer) kryo.getSerializer(TestDefault.class);
38+
assertNotNull(serializer.getField("a"));
39+
serializer.removeField("a");
40+
assertFieldRemoved(serializer, "a");
41+
}
42+
43+
public void testDefaultStrategyForExtendedClass() {
44+
TestExtended testExtended = new TestExtended();
45+
((TestDefault) testExtended).a = "someDefaultValue";
46+
testExtended.a = "someExtendedValue";
47+
kryo.setDefaultSerializer(FieldSerializer.class);
48+
kryo.register(TestExtended.class);
49+
50+
roundTrip(34, 34, testExtended);
51+
52+
FieldSerializer serializer = (FieldSerializer) kryo.getSerializer(TestExtended.class);
53+
54+
// the "a" field needs to be removed 2x, once for TestDefault.a and once for TestExtended.a. You
55+
// can't remove the second one without removing the first one (in DEFAULT field name strategy)
56+
assertNotNull(serializer.getField("a"));
57+
serializer.removeField("a");
58+
assertNotNull(serializer.getField("a"));
59+
serializer.removeField("a");
60+
assertFieldRemoved(serializer, "a");
61+
}
62+
63+
public void testExtendedStrategyForExtendedClass() {
64+
TestExtended testExtended = new TestExtended();
65+
((TestDefault) testExtended).a = "someDefaultValue";
66+
testExtended.a = "someExtendedValue";
67+
kryo.getFieldSerializerConfig().setCachedFieldNameStrategy(FieldSerializer.CachedFieldNameStrategy.EXTENDED);
68+
kryo.setDefaultSerializer(FieldSerializer.class);
69+
kryo.register(TestExtended.class);
70+
71+
roundTrip(34, 34, testExtended);
72+
73+
FieldSerializer serializer = (FieldSerializer) kryo.getSerializer(TestExtended.class);
74+
75+
// Simple class name is part of field name in EXTENDED field name strategy.
76+
assertNotNull(serializer.getField("TestDefault.a"));
77+
serializer.removeField("TestDefault.a");
78+
assertFieldRemoved(serializer, "TestDefault.a");
79+
assertNotNull(serializer.getField("TestExtended.a"));
80+
serializer.removeField("TestExtended.a");
81+
assertFieldRemoved(serializer, "TestExtended.a");
82+
}
83+
84+
protected void assertFieldRemoved(FieldSerializer serializer, String fieldName) {
85+
try {
86+
assertNull(serializer.getField(fieldName));
87+
Assert.fail("Expected IllegalArgumentException to be thrown for serializer.getField(" + fieldName + ")");
88+
} catch (IllegalArgumentException iae) {
89+
assertTrue(true);
90+
}
91+
}
92+
93+
static public class TestDefault {
94+
private String a;
95+
96+
public String getA() {
97+
return a;
98+
}
99+
100+
public void setA(String a) {
101+
this.a = a;
102+
}
103+
104+
@Override
105+
public boolean equals(Object o) {
106+
if (this == o) return true;
107+
if (o == null || getClass() != o.getClass()) return false;
108+
109+
TestDefault that = (TestDefault) o;
110+
111+
return a != null ? a.equals(that.a) : that.a == null;
112+
113+
}
114+
115+
@Override
116+
public int hashCode() {
117+
return a != null ? a.hashCode() : 0;
118+
}
119+
}
120+
121+
static public class TestExtended extends TestDefault {
122+
private String a;
123+
124+
public String getA() {
125+
return a;
126+
}
127+
128+
public void setA(String a) {
129+
this.a = a;
130+
}
131+
132+
@Override
133+
public boolean equals(Object o) {
134+
if (this == o) return true;
135+
if (o == null || getClass() != o.getClass()) return false;
136+
137+
if (!super.equals(o)) return false;
138+
139+
TestExtended that = (TestExtended) o;
140+
return a != null ? a.equals(that.a) : that.a == null;
141+
}
142+
143+
@Override
144+
public int hashCode() {
145+
return a != null ? a.hashCode() : 0;
146+
}
147+
}
148+
}

test/com/esotericsoftware/kryo/FieldSerializerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,7 @@ public void testMultipleTimesAnnotatedMapFields () {
621621

622622
assertFalse("Exception was expected", true);
623623
}
624-
624+
625625
static public class DefaultTypes {
626626
// Primitives.
627627
public boolean booleanField;

0 commit comments

Comments
 (0)