This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new 8b56bc53f fix(java): preserve collection TypeDef serializer family
(#3827)
8b56bc53f is described below
commit 8b56bc53f758ee4a5a33ab78e764887fcac185ce
Author: Shawn Yang <[email protected]>
AuthorDate: Wed Jul 8 22:33:46 2026 +0530
fix(java): preserve collection TypeDef serializer family (#3827)
## Why?
## What does this PR do?
## Related issues
Closes #3826
## AI Contribution Checklist
- [ ] Substantial AI assistance was used in this PR: `yes` / `no`
- [ ] If `yes`, I included a completed [AI Contribution
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
in this PR description and the required `AI Usage Disclosure`.
- [ ] If `yes`, my PR description includes the required `ai_review`
summary and screenshot evidence or equivalent persisted links of the
final clean AI review results from both fresh reviewers described in
`AI_POLICY.md`, the Fory-guided reviewer and the independent general
reviewer, on the current PR diff or current HEAD after the latest code
changes.
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
.../org/apache/fory/resolver/ClassResolver.java | 17 +-
.../org/apache/fory/resolver/TypeResolver.java | 31 +-
.../serializer/CompatibleLayerSerializerBase.java | 13 +
.../collection/ChildContainerSerializers.java | 73 ++--
.../collection/CollectionSerializers.java | 30 +-
.../fory/serializer/collection/MapSerializers.java | 29 +-
.../CollectionCompatibleSerializerTest.java | 377 +++++++++++++++++++++
7 files changed, 535 insertions(+), 35 deletions(-)
diff --git
a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
index 7b12fae6b..e5a144707 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
@@ -954,7 +954,13 @@ public class ClassResolver extends TypeResolver {
if (hasFieldMetadata) {
// Preserve the normal TypeInfo/name cache so locally generated or
dynamically registered
// classes can be resolved when the TypeDef is decoded by the same
resolver.
- getTypeIdForTypeDef(cls);
+ int typeId = getTypeIdForTypeDef(cls);
+ if (usesNonStructTypeDef(cls)) {
+ // Field metadata does not make collection/map/custom-serializer
classes struct-owned.
+ // Their natural serializers may delegate field IO internally, but the
TypeDef root kind
+ // must still resolve to the non-struct serializer family on the
reader.
+ return normalizeTypeDefRootTypeId(cls, typeId);
+ }
return getFieldMetadataTypeIdForTypeDef(cls);
}
TypeInfo typeInfo = classInfoMap.get(cls);
@@ -2011,7 +2017,9 @@ public class ClassResolver extends TypeResolver {
TypeDef typeDef;
Preconditions.checkArgument(
serializerClass !=
UnknownClassSerializers.UnknownStructSerializer.class);
- if (needToWriteTypeDef(serializerClass)) {
+ if (needToWriteTypeDef(serializerClass) ||
needsCollectionFieldTypeDef(serializerClass)) {
+ // Default collection/map serializers remain non-struct roots, but their
wrapper fields still
+ // need TypeDef metadata so remote compatible readers can evolve those
fields.
typeDef = typeDefMap.computeIfAbsent(typeInfo.type, cls ->
TypeDef.buildTypeDef(this, cls));
} else {
// Some type will use other serializers such MapSerializer and so on.
@@ -2026,6 +2034,11 @@ public class ClassResolver extends TypeResolver {
return typeDef;
}
+ private static boolean needsCollectionFieldTypeDef(Class<? extends
Serializer> serializerClass) {
+ return serializerClass ==
CollectionSerializers.DefaultJavaCollectionSerializer.class
+ || serializerClass == MapSerializers.DefaultJavaMapSerializer.class;
+ }
+
/**
* Write classname for java serialization. Note that the object of provided
class can be
* non-serializable, and class with writeReplace/readResolve defined won't
be skipped. For
diff --git
a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
index fde985f4e..a78a38f73 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
@@ -93,6 +93,8 @@ import org.apache.fory.serializer.UnknownClass;
import org.apache.fory.serializer.UnknownClass.UnknownEmptyStruct;
import org.apache.fory.serializer.UnknownClass.UnknownStruct;
import org.apache.fory.serializer.UnknownClassSerializers;
+import org.apache.fory.serializer.collection.CollectionSerializers;
+import org.apache.fory.serializer.collection.MapSerializers;
import org.apache.fory.type.Descriptor;
import org.apache.fory.type.DescriptorBuilder;
import org.apache.fory.type.DescriptorGrouper;
@@ -1136,7 +1138,10 @@ public abstract class TypeResolver {
TypeInfo typeInfo;
if (!typeDef.isStructSchemaKind()
&&
!UnknownClass.class.isAssignableFrom(TypeUtils.getComponentIfArray(cls))) {
- typeInfo = getTypeInfo(cls);
+ typeInfo =
+ typeDef.getFieldsInfo().isEmpty()
+ ? getTypeInfo(cls)
+ : getMetaSharedTypeInfo(typeDef, cls);
} else if (ClassResolver.useReplaceResolveSerializer(cls)) {
// For classes with writeReplace/readResolve, use their natural
serializer
// (ReplaceResolveSerializer) instead of CompatibleSerializer
@@ -1194,6 +1199,14 @@ public abstract class TypeResolver {
|| (Types.isUnionType(streamTypeId) &&
!Types.isUnionType(localTypeInfo.typeId))) {
throw new SerializerUnregisteredException(typeDef.getClassName());
}
+ Serializer<?> serializer = newMetaSharedExtSerializer(cls, typeDef,
localTypeInfo);
+ if (serializer != null) {
+ TypeInfo typeInfo =
+ new TypeInfo(this, cls, null, localTypeInfo.typeId,
typeDef.getClassSpec().userTypeId);
+ typeInfo.typeDef = typeDef;
+ typeInfo.setSerializer(this, serializer);
+ return typeInfo;
+ }
return localTypeInfo;
}
Integer classId = extRegistry.registeredClassIdMap.get(cls);
@@ -1301,6 +1314,22 @@ public abstract class TypeResolver {
return typeInfo;
}
+ private Serializer<?> newMetaSharedExtSerializer(
+ Class<?> cls, TypeDef typeDef, TypeInfo localTypeInfo) {
+ if (typeDef.getFieldsInfo().isEmpty()) {
+ return null;
+ }
+ Serializer<?> serializer = localTypeInfo.getSerializer();
+ Class<?> serializerClass = serializer.getClass();
+ if (serializerClass ==
CollectionSerializers.DefaultJavaCollectionSerializer.class) {
+ return new CollectionSerializers.DefaultJavaCollectionSerializer<>(this,
cls, typeDef);
+ }
+ if (serializerClass == MapSerializers.DefaultJavaMapSerializer.class) {
+ return new MapSerializers.DefaultJavaMapSerializer<>(this, cls, typeDef);
+ }
+ return null;
+ }
+
private Serializer<?> newGeneratedCompatibleSerializer(
Class<?> cls, Class<? extends Serializer> serializerClass, TypeDef
typeDef) {
try {
diff --git
a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java
b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java
index 67fab5f10..3ba12f6a5 100644
---
a/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java
+++
b/java/fory-core/src/main/java/org/apache/fory/serializer/CompatibleLayerSerializerBase.java
@@ -19,6 +19,7 @@
package org.apache.fory.serializer;
+import org.apache.fory.annotation.Internal;
import org.apache.fory.collection.IdentityObjectIntMap;
import org.apache.fory.collection.ObjectIntMap;
import org.apache.fory.context.CopyContext;
@@ -156,6 +157,18 @@ public abstract class CompatibleLayerSerializerBase<T>
extends AbstractObjectSer
return allFields.length;
}
+ @Internal
+ public final TypeDef getLayerTypeDef() {
+ checkLayerSerializerMeta();
+ return layerTypeDef;
+ }
+
+ @Internal
+ public final Class<?> getLayerMarkerClass() {
+ checkLayerSerializerMeta();
+ return layerMarkerClass;
+ }
+
@SuppressWarnings("rawtypes")
public void populateFieldInfo(ObjectIntMap fieldIndexMap, Class<?>[]
fieldTypes) {
checkLayerSerializerMeta();
diff --git
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java
index 49c65b56c..a307d496c 100644
---
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java
+++
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/ChildContainerSerializers.java
@@ -25,6 +25,7 @@ import java.lang.invoke.MethodHandle;
import java.lang.reflect.Field;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
@@ -54,9 +55,11 @@ import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.meta.TypeDef;
import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.resolver.ClassResolver;
+import org.apache.fory.resolver.TypeInfo;
import org.apache.fory.resolver.TypeResolver;
import org.apache.fory.serializer.AbstractObjectSerializer;
import org.apache.fory.serializer.CompatibleLayerSerializer;
+import org.apache.fory.serializer.CompatibleLayerSerializerBase;
import org.apache.fory.serializer.FieldGroups;
import org.apache.fory.serializer.FieldGroups.SerializationFieldInfo;
import org.apache.fory.serializer.JavaSerializer;
@@ -645,12 +648,11 @@ public class ChildContainerSerializers {
readAndCheckNumClassLayers(readContext, collection.getClass(),
slotsSerializers.length);
for (Serializer slotsSerializer : slotsSerializers) {
if (slotsSerializer instanceof CompatibleLayerSerializer) {
- CompatibleLayerSerializer compatibleSerializer =
- (CompatibleLayerSerializer) slotsSerializer;
- // Read layer class meta first if meta share is enabled
- // This corresponds to writeLayerClassMeta() in
CompatibleLayerSerializer.write()
+ CompatibleLayerSerializerBase compatibleSerializer =
+ (CompatibleLayerSerializerBase) slotsSerializer;
if (typeResolver.getConfig().isMetaShareEnabled()) {
- readAndSkipLayerClassMeta(readContext);
+ compatibleSerializer =
+ readLayerSerializer(readContext, typeResolver,
compatibleSerializer);
}
compatibleSerializer.readAndSetFields(readContext, collection);
} else {
@@ -686,16 +688,16 @@ public class ChildContainerSerializers {
}
/**
- * Read and skip the layer class meta from buffer. This is used to skip over
the class definition
- * that was written by CompatibleLayerSerializer.writeLayerClassMeta(). For
- * ChildContainerSerializers, we use the same serializer on both write and
read sides, so we just
- * need to skip the meta without actually parsing it.
+ * Read the layer class meta that was written by
CompatibleLayerSerializer.writeLayerClassMeta().
*/
- private static void readAndSkipLayerClassMeta(ReadContext readContext) {
+ private static CompatibleLayerSerializerBase readLayerSerializer(
+ ReadContext readContext,
+ TypeResolver typeResolver,
+ CompatibleLayerSerializerBase localSerializer) {
MemoryBuffer buffer = readContext.getBuffer();
MetaReadContext metaReadContext = readContext.getMetaReadContext();
if (metaReadContext == null) {
- return;
+ return localSerializer;
}
int indexMarker = buffer.readVarUInt32Small14();
boolean isRef = (indexMarker & 1) == 1;
@@ -704,16 +706,47 @@ public class ChildContainerSerializers {
if (index >= metaReadContext.readTypeInfos.size) {
throw new ForyException("Invalid layer metadata reference id " +
index);
}
- // Reference to previously read type - nothing more to read
- return;
+ TypeInfo typeInfo = metaReadContext.readTypeInfos.get(index);
+ if (typeInfo == null) {
+ throw new ForyException("Invalid layer metadata reference id " +
index);
+ }
+ return getLayerSerializer(typeResolver, localSerializer, typeInfo);
}
- // New type - need to read and skip the TypeDef bytes
long id = buffer.readInt64();
- TypeDef.skipTypeDef(buffer, id);
- // Add a placeholder to keep readTypeInfos indices in sync with the write
side's classMap.
- // The write side (writeLayerClassMeta) adds layer marker classes to
classMap which shares
- // the same index space as writeSharedClassMeta. Without this placeholder,
subsequent
- // readSharedClassMeta reference lookups would use wrong indices.
- metaReadContext.readTypeInfos.add(null);
+ TypeInfo typeInfo = readLayerTypeInfo(typeResolver, buffer,
localSerializer, id);
+ metaReadContext.readTypeInfos.add(typeInfo);
+ return getLayerSerializer(typeResolver, localSerializer, typeInfo);
+ }
+
+ private static TypeInfo readLayerTypeInfo(
+ TypeResolver typeResolver,
+ MemoryBuffer buffer,
+ CompatibleLayerSerializerBase localSerializer,
+ long typeDefId) {
+ TypeDef localTypeDef = localSerializer.getLayerTypeDef();
+ byte[] encoded = TypeDef.readTypeDefBytes(typeResolver, buffer, typeDefId);
+ Class<?> layerClass = localSerializer.getType();
+ typeResolver.checkClassForDeserialization(layerClass);
+ TypeDef typeDef =
+ Arrays.equals(encoded, localTypeDef.getEncoded())
+ ? localTypeDef
+ :
typeResolver.cacheRemoteTypeDef(TypeDef.readTypeDef(typeResolver, encoded));
+ return new TypeInfo(layerClass, typeDef);
+ }
+
+ private static CompatibleLayerSerializerBase getLayerSerializer(
+ TypeResolver typeResolver, CompatibleLayerSerializerBase
localSerializer, TypeInfo typeInfo) {
+ Serializer<?> serializer = typeInfo.getSerializer();
+ if (serializer != null) {
+ return (CompatibleLayerSerializerBase) serializer;
+ }
+ CompatibleLayerSerializer<?> newSerializer =
+ new CompatibleLayerSerializer(
+ typeResolver,
+ localSerializer.getType(),
+ typeInfo.getTypeDef(),
+ localSerializer.getLayerMarkerClass());
+ typeInfo.setSerializer(newSerializer);
+ return newSerializer;
}
}
diff --git
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java
index 5dc330494..81c2355bb 100644
---
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java
+++
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionSerializers.java
@@ -56,12 +56,14 @@ import org.apache.fory.exception.DeserializationException;
import org.apache.fory.exception.ForyException;
import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.memory.MemoryUtils;
+import org.apache.fory.meta.TypeDef;
import org.apache.fory.reflect.FieldAccessor;
import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.resolver.ClassResolver;
import org.apache.fory.resolver.TypeInfo;
import org.apache.fory.resolver.TypeInfoHolder;
import org.apache.fory.resolver.TypeResolver;
+import org.apache.fory.serializer.CompatibleSerializer;
import org.apache.fory.serializer.ExternalizableSerializer;
import org.apache.fory.serializer.GraphMemoryEstimates;
import org.apache.fory.serializer.ReplaceResolveSerializer;
@@ -1050,17 +1052,33 @@ public class CollectionSerializers {
private Serializer<T> dataSerializer;
public DefaultJavaCollectionSerializer(TypeResolver typeResolver, Class<T>
cls) {
+ this(typeResolver, cls, null, true);
+ }
+
+ public DefaultJavaCollectionSerializer(
+ TypeResolver typeResolver, Class<T> cls, TypeDef typeDef) {
+ this(typeResolver, cls, typeDef, false);
+ }
+
+ private DefaultJavaCollectionSerializer(
+ TypeResolver typeResolver, Class<T> cls, TypeDef typeDef, boolean
registerSerializer) {
super(typeResolver, cls, false);
Preconditions.checkArgument(
!config.isXlang(),
"Fory cross-language default collection serializer should use "
+ CollectionSerializer.class);
- typeResolver.setSerializer(cls, this);
- Class<? extends Serializer> serializerClass =
- ((ClassResolver) typeResolver)
- .getObjectSerializerClass(
- cls, sc -> dataSerializer =
Serializers.newSerializer(typeResolver, cls, sc));
- dataSerializer = Serializers.newSerializer(typeResolver, cls,
serializerClass);
+ if (registerSerializer) {
+ typeResolver.setSerializer(cls, this);
+ }
+ if (typeDef == null) {
+ Class<? extends Serializer> serializerClass =
+ ((ClassResolver) typeResolver)
+ .getObjectSerializerClass(
+ cls, sc -> dataSerializer =
Serializers.newSerializer(typeResolver, cls, sc));
+ dataSerializer = Serializers.newSerializer(typeResolver, cls,
serializerClass);
+ } else {
+ dataSerializer = new CompatibleSerializer<>(typeResolver, cls,
typeDef);
+ }
// No need to set object serializer to this, it will be set in class
resolver later.
// typeResolver.setSerializer(cls, this);
}
diff --git
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java
index afc4c3647..2e402e061 100644
---
a/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java
+++
b/java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapSerializers.java
@@ -49,10 +49,12 @@ import org.apache.fory.context.CopyContext;
import org.apache.fory.context.ReadContext;
import org.apache.fory.context.WriteContext;
import org.apache.fory.memory.MemoryBuffer;
+import org.apache.fory.meta.TypeDef;
import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.resolver.ClassResolver;
import org.apache.fory.resolver.TypeInfo;
import org.apache.fory.resolver.TypeResolver;
+import org.apache.fory.serializer.CompatibleSerializer;
import org.apache.fory.serializer.ExternalizableSerializer;
import org.apache.fory.serializer.GraphMemoryEstimates;
import org.apache.fory.serializer.ReplaceResolveSerializer;
@@ -510,16 +512,31 @@ public class MapSerializers {
private Serializer<T> dataSerializer;
public DefaultJavaMapSerializer(TypeResolver typeResolver, Class<T> cls) {
+ this(typeResolver, cls, null, true);
+ }
+
+ public DefaultJavaMapSerializer(TypeResolver typeResolver, Class<T> cls,
TypeDef typeDef) {
+ this(typeResolver, cls, typeDef, false);
+ }
+
+ private DefaultJavaMapSerializer(
+ TypeResolver typeResolver, Class<T> cls, TypeDef typeDef, boolean
registerSerializer) {
super(typeResolver, cls, false);
Preconditions.checkArgument(
!config.isXlang(),
"Fory cross-language default map serializer should use " +
MapSerializer.class);
- typeResolver.setSerializer(cls, this);
- Class<? extends Serializer> serializerClass =
- ((ClassResolver) typeResolver)
- .getObjectSerializerClass(
- cls, sc -> dataSerializer =
Serializers.newSerializer(typeResolver, cls, sc));
- dataSerializer = Serializers.newSerializer(typeResolver, cls,
serializerClass);
+ if (registerSerializer) {
+ typeResolver.setSerializer(cls, this);
+ }
+ if (typeDef == null) {
+ Class<? extends Serializer> serializerClass =
+ ((ClassResolver) typeResolver)
+ .getObjectSerializerClass(
+ cls, sc -> dataSerializer =
Serializers.newSerializer(typeResolver, cls, sc));
+ dataSerializer = Serializers.newSerializer(typeResolver, cls,
serializerClass);
+ } else {
+ dataSerializer = new CompatibleSerializer<>(typeResolver, cls,
typeDef);
+ }
// No need to set object serializer to this, it will be set in class
resolver later.
// typeResolver.setSerializer(cls, this);
}
diff --git
a/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionCompatibleSerializerTest.java
b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionCompatibleSerializerTest.java
new file mode 100644
index 000000000..930727888
--- /dev/null
+++
b/java/fory-core/src/test/java/org/apache/fory/serializer/collection/CollectionCompatibleSerializerTest.java
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fory.serializer.collection;
+
+import java.io.ByteArrayOutputStream;
+import java.lang.reflect.Field;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.tools.JavaCompiler;
+import javax.tools.ToolProvider;
+import org.apache.fory.Fory;
+import org.apache.fory.ForyTestBase;
+import org.apache.fory.context.MetaReadContext;
+import org.apache.fory.context.MetaWriteContext;
+import org.apache.fory.util.ClassLoaderUtils;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class CollectionCompatibleSerializerTest extends ForyTestBase {
+ private static final String PEER_PACKAGE =
"org.apache.fory.serializer.collection.compat";
+
+ @Test
+ public void testCustomSetCompatible() {
+ Fory writer = compatibleFory();
+ Fory reader = compatibleFory();
+ CustomSet set = new CustomSet("set-marker");
+ set.add("a");
+ set.add("b");
+
+ SetHolder holder = new SetHolder();
+ holder.values = set;
+
+ SetHolder copy = serDe(writer, reader, holder);
+ Assert.assertEquals(copy.values.getClass(), CustomSet.class);
+ CustomSet copySet = (CustomSet) copy.values;
+ Assert.assertEquals(copySet.getMarker(), "set-marker");
+ Assert.assertEquals(copySet, set);
+ }
+
+ @Test
+ public void testCustomMapCompatible() {
+ Fory writer = compatibleFory();
+ Fory reader = compatibleFory();
+ CustomMap map = new CustomMap("map-marker");
+ map.put("k1", "v1");
+ map.put("k2", "v2");
+
+ MapHolder holder = new MapHolder();
+ holder.values = map;
+
+ MapHolder copy = serDe(writer, reader, holder);
+ Assert.assertEquals(copy.values.getClass(), CustomMap.class);
+ CustomMap copyMap = (CustomMap) copy.values;
+ Assert.assertEquals(copyMap.getMarker(), "map-marker");
+ Assert.assertEquals(copyMap, map);
+ }
+
+ @Test
+ public void testDefaultSetEvolution() throws Exception {
+ Class<?> writerPeer = compilePeer("DefaultSetPeer",
defaultSetSource(true));
+ Class<?> readerPeer = compilePeer("DefaultSetPeer",
defaultSetSource(false));
+ Object writerHolder = writerPeer.getMethod("newHolder").invoke(null);
+
+ Object copy = peerSerDe(writerPeer.getClassLoader(),
readerPeer.getClassLoader(), writerHolder);
+ Object values = fieldValue(copy, "values");
+ Assert.assertEquals(values.getClass().getName(), PEER_PACKAGE +
".DefaultSetPeer$CustomSet");
+ Assert.assertEquals(fieldValue(values, "marker"), "set-marker");
+ Assert.assertEquals(((Collection<?>) values).size(), 2);
+ Assert.assertTrue(((Collection<?>) values).contains("a"));
+ }
+
+ @Test
+ public void testDefaultMapEvolution() throws Exception {
+ Class<?> writerPeer = compilePeer("DefaultMapPeer",
defaultMapSource(true));
+ Class<?> readerPeer = compilePeer("DefaultMapPeer",
defaultMapSource(false));
+ Object writerHolder = writerPeer.getMethod("newHolder").invoke(null);
+
+ Object copy = peerSerDe(writerPeer.getClassLoader(),
readerPeer.getClassLoader(), writerHolder);
+ Object values = fieldValue(copy, "values");
+ Assert.assertEquals(values.getClass().getName(), PEER_PACKAGE +
".DefaultMapPeer$CustomMap");
+ Assert.assertEquals(fieldValue(values, "marker"), "map-marker");
+ Assert.assertEquals(((Map<?, ?>) values).get("k1"), "v1");
+ Assert.assertEquals(((Map<?, ?>) values).size(), 2);
+ }
+
+ @Test
+ public void testChildSetEvolution() throws Exception {
+ Class<?> writerPeer = compilePeer("ChildSetPeer", childSetSource(true));
+ Class<?> readerPeer = compilePeer("ChildSetPeer", childSetSource(false));
+ Object writerHolder = writerPeer.getMethod("newHolder").invoke(null);
+
+ Object copy = peerSerDe(writerPeer.getClassLoader(),
readerPeer.getClassLoader(), writerHolder);
+ Object values = fieldValue(copy, "values");
+ Assert.assertEquals(values.getClass().getName(), PEER_PACKAGE +
".ChildSetPeer$CustomSet");
+ Assert.assertEquals(fieldValue(values, "marker"), "child-set-marker");
+ Assert.assertTrue(((Collection<?>) values).contains("a"));
+ }
+
+ @Test
+ public void testChildMapEvolution() throws Exception {
+ Class<?> writerPeer = compilePeer("ChildMapPeer", childMapSource(true));
+ Class<?> readerPeer = compilePeer("ChildMapPeer", childMapSource(false));
+ Object writerHolder = writerPeer.getMethod("newHolder").invoke(null);
+
+ Object copy = peerSerDe(writerPeer.getClassLoader(),
readerPeer.getClassLoader(), writerHolder);
+ Object values = fieldValue(copy, "values");
+ Assert.assertEquals(values.getClass().getName(), PEER_PACKAGE +
".ChildMapPeer$CustomMap");
+ Assert.assertEquals(fieldValue(values, "marker"), "child-map-marker");
+ Assert.assertEquals(((Map<?, ?>) values).get("k1"), "v1");
+ }
+
+ @Test
+ public void testObjectStreamEvolution() throws Exception {
+ Class<?> writerPeer = compilePeer("StreamPeer", streamSource(true));
+ Class<?> readerPeer = compilePeer("StreamPeer", streamSource(false));
+ Object writerValue = writerPeer.getMethod("newValue").invoke(null);
+
+ Object copy = peerSerDe(writerPeer.getClassLoader(),
readerPeer.getClassLoader(), writerValue);
+ Assert.assertEquals(copy.getClass().getName(), PEER_PACKAGE +
".StreamPeer");
+ Assert.assertEquals(fieldValue(copy, "marker"), "stream-marker");
+ }
+
+ private static Fory compatibleFory() {
+ return
builder().withRefTracking(true).withCodegen(true).withCompatible(true).build();
+ }
+
+ private static Fory peerFory(ClassLoader classLoader) {
+ return builder()
+ .withClassLoader(classLoader)
+ .withRefTracking(true)
+ .withCodegen(true)
+ .withCompatible(true)
+ .withMetaShare(true)
+ .withScopedMetaShare(false)
+ .build();
+ }
+
+ private static Object peerSerDe(
+ ClassLoader writerClassLoader, ClassLoader readerClassLoader, Object
value) {
+ Fory writer = peerFory(writerClassLoader);
+ Fory reader = peerFory(readerClassLoader);
+ MetaWriteContext metaWriteContext = new MetaWriteContext();
+ MetaReadContext metaReadContext = new MetaReadContext();
+ setMetaContexts(writer, metaWriteContext, new MetaReadContext());
+ byte[] bytes = writer.serialize(value);
+ setMetaContexts(reader, new MetaWriteContext(), metaReadContext);
+ return reader.deserialize(bytes);
+ }
+
+ private static Object fieldValue(Object obj, String name) throws
ReflectiveOperationException {
+ Field field = obj.getClass().getField(name);
+ return field.get(obj);
+ }
+
+ private static Class<?> compilePeer(String className, String source) throws
Exception {
+ Path dir = Files.createTempDirectory("fory-compatible-collection");
+ Path sourceFile = dir.resolve(PEER_PACKAGE.replace('.',
'/')).resolve(className + ".java");
+ Files.createDirectories(sourceFile.getParent());
+ Files.write(sourceFile, source.getBytes(StandardCharsets.UTF_8));
+ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ Assert.assertNotNull(compiler, "Tests require a JDK compiler");
+ int result =
+ compiler.run(
+ null,
+ new ByteArrayOutputStream(),
+ System.err,
+ "-classpath",
+ System.getProperty("java.class.path"),
+ "-d",
+ dir.toString(),
+ sourceFile.toString());
+ if (result != 0) {
+ throw new AssertionError("Couldn't compile code:\n" + source);
+ }
+ URL[] urls = new URL[] {dir.toUri().toURL()};
+ ClassLoader parent =
CollectionCompatibleSerializerTest.class.getClassLoader();
+ return new ClassLoaderUtils.ChildFirstURLClassLoader(urls, parent)
+ .loadClass(PEER_PACKAGE + "." + className);
+ }
+
+ private static String defaultSetSource(boolean writer) {
+ return sourceHeader("DefaultSetPeer")
+ + " public static class Holder { public Set values; }\n"
+ + " public static class CustomSet extends AbstractSet {\n"
+ + " public LinkedHashSet values = new LinkedHashSet();\n"
+ + " public String marker;\n"
+ + (writer ? " public String writerOnly;\n" : "")
+ + " public Iterator iterator() { return values.iterator(); }\n"
+ + " public int size() { return values.size(); }\n"
+ + " public boolean add(Object value) { return values.add(value);
}\n"
+ + " }\n"
+ + " public static Holder newHolder() {\n"
+ + " Holder holder = new Holder();\n"
+ + " CustomSet values = new CustomSet();\n"
+ + " values.marker = \"set-marker\";\n"
+ + (writer ? " values.writerOnly = \"writer-only\";\n" : "")
+ + " values.add(\"a\"); values.add(\"b\"); holder.values = values;
return holder;\n"
+ + " }\n"
+ + "}\n";
+ }
+
+ private static String defaultMapSource(boolean writer) {
+ return sourceHeader("DefaultMapPeer")
+ + " public static class Holder { public Map values; }\n"
+ + " public static class CustomMap extends AbstractMap {\n"
+ + " public LinkedHashMap values = new LinkedHashMap();\n"
+ + " public String marker;\n"
+ + (writer ? " public String writerOnly;\n" : "")
+ + " public Object put(Object key, Object value) { return
values.put(key, value); }\n"
+ + " public Set entrySet() { return values.entrySet(); }\n"
+ + " }\n"
+ + " public static Holder newHolder() {\n"
+ + " Holder holder = new Holder();\n"
+ + " CustomMap values = new CustomMap();\n"
+ + " values.marker = \"map-marker\";\n"
+ + (writer ? " values.writerOnly = \"writer-only\";\n" : "")
+ + " values.put(\"k1\", \"v1\"); values.put(\"k2\", \"v2\");\n"
+ + " holder.values = values; return holder;\n"
+ + " }\n"
+ + "}\n";
+ }
+
+ private static String childSetSource(boolean writer) {
+ return sourceHeader("ChildSetPeer")
+ + " public static class Holder { public Set values; }\n"
+ + " public static class CustomSet extends HashSet {\n"
+ + " public String marker;\n"
+ + (writer ? " public String writerOnly;\n" : "")
+ + " }\n"
+ + " public static Holder newHolder() {\n"
+ + " Holder holder = new Holder();\n"
+ + " CustomSet values = new CustomSet();\n"
+ + " values.marker = \"child-set-marker\";\n"
+ + (writer ? " values.writerOnly = \"writer-only\";\n" : "")
+ + " values.add(\"a\"); values.add(\"b\"); holder.values = values;
return holder;\n"
+ + " }\n"
+ + "}\n";
+ }
+
+ private static String childMapSource(boolean writer) {
+ return sourceHeader("ChildMapPeer")
+ + " public static class Holder { public Map values; }\n"
+ + " public static class CustomMap extends HashMap {\n"
+ + " public String marker;\n"
+ + (writer ? " public String writerOnly;\n" : "")
+ + " }\n"
+ + " public static Holder newHolder() {\n"
+ + " Holder holder = new Holder();\n"
+ + " CustomMap values = new CustomMap();\n"
+ + " values.marker = \"child-map-marker\";\n"
+ + (writer ? " values.writerOnly = \"writer-only\";\n" : "")
+ + " values.put(\"k1\", \"v1\"); values.put(\"k2\", \"v2\");\n"
+ + " holder.values = values; return holder;\n"
+ + " }\n"
+ + "}\n";
+ }
+
+ private static String streamSource(boolean writer) {
+ return sourceHeader("StreamPeer")
+ + " public String marker;\n"
+ + (writer ? " public String writerOnly;\n" : "")
+ + " private void writeObject(ObjectOutputStream out) throws
IOException {\n"
+ + " out.defaultWriteObject();\n"
+ + " }\n"
+ + " private void readObject(ObjectInputStream in)\n"
+ + " throws IOException, ClassNotFoundException {\n"
+ + " in.defaultReadObject();\n"
+ + " }\n"
+ + " public static StreamPeer newValue() {\n"
+ + " StreamPeer value = new StreamPeer(); value.marker =
\"stream-marker\";\n"
+ + (writer ? " value.writerOnly = \"writer-only\";\n" : "")
+ + " return value;\n"
+ + " }\n"
+ + "}\n";
+ }
+
+ private static String sourceHeader(String className) {
+ return "package "
+ + PEER_PACKAGE
+ + ";\n"
+ + "import java.io.*;\n"
+ + "import java.util.*;\n"
+ + "public class "
+ + className
+ + " {\n";
+ }
+
+ public static final class SetHolder {
+ public Set<String> values;
+ }
+
+ public static final class MapHolder {
+ public Map<String, String> values;
+ }
+
+ public static final class CustomSet extends AbstractSet<String> {
+ private LinkedHashSet<String> values = new LinkedHashSet<>();
+ private String marker;
+
+ public CustomSet() {}
+
+ private CustomSet(String marker) {
+ this.marker = marker;
+ }
+
+ public String getMarker() {
+ return marker;
+ }
+
+ @Override
+ public boolean add(String value) {
+ return values.add(value);
+ }
+
+ @Override
+ public Iterator<String> iterator() {
+ return values.iterator();
+ }
+
+ @Override
+ public int size() {
+ return values.size();
+ }
+ }
+
+ public static final class CustomMap extends AbstractMap<String, String> {
+ private LinkedHashMap<String, String> values = new LinkedHashMap<>();
+ private String marker;
+
+ public CustomMap() {}
+
+ private CustomMap(String marker) {
+ this.marker = marker;
+ }
+
+ public String getMarker() {
+ return marker;
+ }
+
+ @Override
+ public String put(String key, String value) {
+ return values.put(key, value);
+ }
+
+ @Override
+ public Set<Entry<String, String>> entrySet() {
+ return values.entrySet();
+ }
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]