From 888920373463877bab4f4114fc90a780fbc175fe Mon Sep 17 00:00:00 2001 From: orisgarno <42269241+orisgarno@users.noreply.github.com> Date: Sat, 1 Feb 2025 21:38:51 +0700 Subject: [PATCH] feat(java): deserialize one pojo into another type (#2012) ## What does this PR do? replace class def if target class is different type with the actual serialized one, so it can be deserialized to another type #1998 ## Related issues ## 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 --------- Co-authored-by: chaokunyang --- docs/guide/java_serialization_guide.md | 4 +- .../src/main/java/org/apache/fury/Fury.java | 2 +- .../java/org/apache/fury/meta/ClassDef.java | 18 +++ .../org/apache/fury/meta/ClassDefEncoder.java | 10 +- .../apache/fury/resolver/ClassResolver.java | 24 ++++ ...DifferentPOJOCompatibleSerializerTest.java | 25 +---- ...patibleSerializerWithRegistrationTest.java | 103 ++++++++++++++++++ 7 files changed, 161 insertions(+), 25 deletions(-) create mode 100644 java/fury-core/src/test/java/org/apache/fury/serializer/compatible/DifferentPOJOCompatibleSerializerWithRegistrationTest.java diff --git a/docs/guide/java_serialization_guide.md b/docs/guide/java_serialization_guide.md index 707643935b..9a792d7b00 100644 --- a/docs/guide/java_serialization_guide.md +++ b/docs/guide/java_serialization_guide.md @@ -521,9 +521,7 @@ consistent between serialization and deserialization. ### Deserialize POJO into another type Fury allows you to serialize one POJO and deserialize it into a different POJO. To achieve this, configure Fury with -`CompatibleMode` set to `org.apache.fury.config.CompatibleMode.COMPATIBLE`. Additionally, you only need to register the -specific classes you want to serialize or deserialize to setup type mapping relationship; there's no need to register any nested classes within them. -[See example here](/java/fury-core/src/test/java/org/apache/fury/serializer/compatible/DifferentPOJOCompatibleSerializerTest.java) +`CompatibleMode` set to `org.apache.fury.config.CompatibleMode.COMPATIBLE`. ### Use wrong API for deserialization diff --git a/java/fury-core/src/main/java/org/apache/fury/Fury.java b/java/fury-core/src/main/java/org/apache/fury/Fury.java index 89e23fc23e..5ccdafd42c 100644 --- a/java/fury-core/src/main/java/org/apache/fury/Fury.java +++ b/java/fury-core/src/main/java/org/apache/fury/Fury.java @@ -1131,7 +1131,7 @@ public T deserializeJavaObject(MemoryBuffer buffer, Class cls) { if (nextReadRefId >= NOT_NULL_VALUE_FLAG) { ClassInfo classInfo; if (shareMeta) { - classInfo = classResolver.readClassInfo(buffer); + classInfo = classResolver.readClassInfoWithMetaShare(buffer, cls); } else { classInfo = classResolver.getClassInfo(cls); } diff --git a/java/fury-core/src/main/java/org/apache/fury/meta/ClassDef.java b/java/fury-core/src/main/java/org/apache/fury/meta/ClassDef.java index 1b6177e004..695d552be7 100644 --- a/java/fury-core/src/main/java/org/apache/fury/meta/ClassDef.java +++ b/java/fury-core/src/main/java/org/apache/fury/meta/ClassDef.java @@ -37,6 +37,7 @@ import java.util.Map; import java.util.Objects; import java.util.SortedMap; +import java.util.stream.Collectors; import org.apache.fury.Fury; import org.apache.fury.builder.MetaSharedCodecBuilder; import org.apache.fury.collection.Tuple2; @@ -787,4 +788,21 @@ public static ClassDef buildClassDef( ClassResolver classResolver, Class type, List fields, boolean isObjectType) { return ClassDefEncoder.buildClassDef(classResolver, type, fields, isObjectType); } + + public ClassDef replaceRootClassTo(ClassResolver classResolver, Class targetCls) { + String name = targetCls.getName(); + List fieldInfos = + fieldsInfo.stream() + .map( + fieldInfo -> { + if (fieldInfo.definedClass.equals(classSpec.entireClassName)) { + return new FieldInfo(name, fieldInfo.fieldName, fieldInfo.fieldType); + } else { + return fieldInfo; + } + }) + .collect(Collectors.toList()); + return ClassDefEncoder.buildClassDefWithFieldInfos( + classResolver, targetCls, fieldInfos, isObjectType); + } } diff --git a/java/fury-core/src/main/java/org/apache/fury/meta/ClassDefEncoder.java b/java/fury-core/src/main/java/org/apache/fury/meta/ClassDefEncoder.java index c1ac5ebdce..d40dd53d9d 100644 --- a/java/fury-core/src/main/java/org/apache/fury/meta/ClassDefEncoder.java +++ b/java/fury-core/src/main/java/org/apache/fury/meta/ClassDefEncoder.java @@ -105,7 +105,15 @@ static List buildFieldsInfo(ClassResolver resolver, List field /** Build class definition from fields of class. */ static ClassDef buildClassDef( ClassResolver classResolver, Class type, List fields, boolean isObjectType) { - List fieldInfos = buildFieldsInfo(classResolver, fields); + return buildClassDefWithFieldInfos( + classResolver, type, buildFieldsInfo(classResolver, fields), isObjectType); + } + + static ClassDef buildClassDefWithFieldInfos( + ClassResolver classResolver, + Class type, + List fieldInfos, + boolean isObjectType) { Map> classLayers = getClassFields(type, fieldInfos); fieldInfos = new ArrayList<>(fieldInfos.size()); classLayers.values().forEach(fieldInfos::addAll); diff --git a/java/fury-core/src/main/java/org/apache/fury/resolver/ClassResolver.java b/java/fury-core/src/main/java/org/apache/fury/resolver/ClassResolver.java index 9b73734e0b..002869809b 100644 --- a/java/fury-core/src/main/java/org/apache/fury/resolver/ClassResolver.java +++ b/java/fury-core/src/main/java/org/apache/fury/resolver/ClassResolver.java @@ -247,6 +247,8 @@ private static class ExtRegistry { private final Map, FieldResolver> fieldResolverMap = new HashMap<>(); private final LongMap> classIdToDef = new LongMap<>(); private final Map, ClassDef> currentLayerClassDef = new HashMap<>(); + // Tuple2: Tuple2 + private final Map, Class>, ClassInfo> transformedClassInfo = new HashMap<>(); // TODO(chaokunyang) Better to use soft reference, see ObjectStreamClass. private final ConcurrentHashMap, Boolean>, SortedMap> descriptorsCache = new ConcurrentHashMap<>(); @@ -1411,6 +1413,27 @@ private ClassInfo readClassInfoWithMetaShare(MetaContext metaContext, int index) return classInfo; } + public ClassInfo readClassInfoWithMetaShare(MemoryBuffer buffer, Class targetClass) { + assert metaContextShareEnabled; + ClassInfo classInfo = + readClassInfoWithMetaShare(buffer, fury.getSerializationContext().getMetaContext()); + Class readClass = classInfo.getCls(); + // replace target class if needed + if (targetClass != readClass) { + Tuple2, Class> key = Tuple2.of(readClass, targetClass); + ClassInfo newClassInfo = extRegistry.transformedClassInfo.get(key); + if (newClassInfo == null) { + // similar to create serializer for `NonexistentMetaShared` + newClassInfo = + getMetaSharedClassInfo( + classInfo.classDef.replaceRootClassTo(this, targetClass), targetClass); + extRegistry.transformedClassInfo.put(key, newClassInfo); + } + return newClassInfo; + } + return classInfo; + } + private ClassInfo buildMetaSharedClassInfo( Tuple2 classDefTuple, ClassDef classDef) { ClassInfo classInfo; @@ -1439,6 +1462,7 @@ private ClassInfo getMetaSharedClassInfo(ClassDef classDef, Class clz) { Short classId = extRegistry.registeredClassIdMap.get(cls); ClassInfo classInfo = new ClassInfo(this, cls, null, classId == null ? NO_CLASS_ID : classId, NOT_SUPPORT_XLANG); + classInfo.classDef = classDef; if (NonexistentClass.class.isAssignableFrom(TypeUtils.getComponentIfArray(cls))) { if (cls == NonexistentMetaShared.class) { classInfo.setSerializer(this, new NonexistentClassSerializer(fury, classDef)); diff --git a/java/fury-core/src/test/java/org/apache/fury/serializer/compatible/DifferentPOJOCompatibleSerializerTest.java b/java/fury-core/src/test/java/org/apache/fury/serializer/compatible/DifferentPOJOCompatibleSerializerTest.java index 3a7436d9c2..59dd15d46a 100644 --- a/java/fury-core/src/test/java/org/apache/fury/serializer/compatible/DifferentPOJOCompatibleSerializerTest.java +++ b/java/fury-core/src/test/java/org/apache/fury/serializer/compatible/DifferentPOJOCompatibleSerializerTest.java @@ -29,8 +29,7 @@ /** * Test COMPATIBILITY mode that supports - same field type and name can be deserialized to other - * class with different name - scrambled field order to make sure it could handle different field - * order - missing or extra field from source class to target class - generic class + * class with different name */ public class DifferentPOJOCompatibleSerializerTest extends Assert { @@ -61,9 +60,9 @@ void testTargetHasLessFieldComparedToSourceClass() throws InterruptedException { ClassCompleteField subclass = new ClassCompleteField<>("subclass", "subclass2"); ClassCompleteField> classCompleteField = new ClassCompleteField<>(subclass, subclass); - byte[] serialized = getFury(ClassCompleteField.class).serializeJavaObject(classCompleteField); + byte[] serialized = getFury().serializeJavaObject(classCompleteField); ClassMissingField> classMissingField = - getFury(ClassMissingField.class).deserializeJavaObject(serialized, ClassMissingField.class); + getFury().deserializeJavaObject(serialized, ClassMissingField.class); assertEq(classCompleteField, classMissingField); } @@ -73,29 +72,15 @@ void testTargetHasMoreFieldComparedToSourceClass() throws InterruptedException { ClassMissingField subclass = new ClassMissingField<>("subclass"); ClassMissingField classMissingField = new ClassMissingField(subclass); - byte[] serialized = getFury(ClassMissingField.class).serializeJavaObject(classMissingField); + byte[] serialized = getFury().serializeJavaObject(classMissingField); ClassCompleteField classCompleteField = - getFury(ClassCompleteField.class) - .deserializeJavaObject(serialized, ClassCompleteField.class); + getFury().deserializeJavaObject(serialized, ClassCompleteField.class); assertEq(classCompleteField, classMissingField); } void assertEq(ClassCompleteField classCompleteField, ClassMissingField classMissingField) { - assertEqSubClass( - (ClassCompleteField) classCompleteField.getPrivateFieldSubClass(), - (ClassMissingField) classMissingField.getPrivateFieldSubClass()); - assertEquals(classCompleteField.getPrivateMap(), classMissingField.getPrivateMap()); - assertEquals(classCompleteField.getPrivateList(), classMissingField.getPrivateList()); - assertEquals(classCompleteField.getPrivateString(), classMissingField.getPrivateString()); - assertEquals(classCompleteField.getPrivateInt(), classMissingField.getPrivateInt()); - } - - void assertEqSubClass( - ClassCompleteField classCompleteField, ClassMissingField classMissingField) { - assertEquals( - classCompleteField.getPrivateFieldSubClass(), classMissingField.getPrivateFieldSubClass()); assertEquals(classCompleteField.getPrivateMap(), classMissingField.getPrivateMap()); assertEquals(classCompleteField.getPrivateList(), classMissingField.getPrivateList()); assertEquals(classCompleteField.getPrivateString(), classMissingField.getPrivateString()); diff --git a/java/fury-core/src/test/java/org/apache/fury/serializer/compatible/DifferentPOJOCompatibleSerializerWithRegistrationTest.java b/java/fury-core/src/test/java/org/apache/fury/serializer/compatible/DifferentPOJOCompatibleSerializerWithRegistrationTest.java new file mode 100644 index 0000000000..c5c8d15efc --- /dev/null +++ b/java/fury-core/src/test/java/org/apache/fury/serializer/compatible/DifferentPOJOCompatibleSerializerWithRegistrationTest.java @@ -0,0 +1,103 @@ +/* + * 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.fury.serializer.compatible; + +import org.apache.fury.Fury; +import org.apache.fury.config.CompatibleMode; +import org.apache.fury.config.Language; +import org.apache.fury.serializer.compatible.classes.ClassCompleteField; +import org.apache.fury.serializer.compatible.classes.ClassMissingField; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * Test COMPATIBILITY mode that supports - same field type and name can be deserialized to other + * also test generic class + */ +public class DifferentPOJOCompatibleSerializerWithRegistrationTest extends Assert { + + Fury getFury(Class... classes) { + Fury instance = + Fury.builder() + .withLanguage(Language.JAVA) + .withRefTracking(true) + .withCompatibleMode(CompatibleMode.COMPATIBLE) + .withMetaShare(true) + .withScopedMetaShare(true) + .requireClassRegistration(false) + .withAsyncCompilation(true) + .serializeEnumByName(true) + .build(); + if (classes != null) { + for (Class clazz : classes) { + instance.register(clazz); + } + } + ; + return instance; + } + + @Test + void testTargetHasLessFieldComparedToSourceClass() throws InterruptedException { + + ClassCompleteField subclass = new ClassCompleteField<>("subclass", "subclass2"); + ClassCompleteField> classCompleteField = + new ClassCompleteField<>(subclass, subclass); + byte[] serialized = getFury(ClassCompleteField.class).serializeJavaObject(classCompleteField); + ClassMissingField> classMissingField = + getFury(ClassMissingField.class).deserializeJavaObject(serialized, ClassMissingField.class); + + assertEq(classCompleteField, classMissingField); + } + + @Test + void testTargetHasMoreFieldComparedToSourceClass() throws InterruptedException { + + ClassMissingField subclass = new ClassMissingField<>("subclass"); + ClassMissingField classMissingField = new ClassMissingField(subclass); + byte[] serialized = getFury(ClassMissingField.class).serializeJavaObject(classMissingField); + + ClassCompleteField classCompleteField = + getFury(ClassCompleteField.class) + .deserializeJavaObject(serialized, ClassCompleteField.class); + + assertEq(classCompleteField, classMissingField); + } + + void assertEq(ClassCompleteField classCompleteField, ClassMissingField classMissingField) { + assertEqSubClass( + (ClassCompleteField) classCompleteField.getPrivateFieldSubClass(), + (ClassMissingField) classMissingField.getPrivateFieldSubClass()); + assertEquals(classCompleteField.getPrivateMap(), classMissingField.getPrivateMap()); + assertEquals(classCompleteField.getPrivateList(), classMissingField.getPrivateList()); + assertEquals(classCompleteField.getPrivateString(), classMissingField.getPrivateString()); + assertEquals(classCompleteField.getPrivateInt(), classMissingField.getPrivateInt()); + } + + void assertEqSubClass( + ClassCompleteField classCompleteField, ClassMissingField classMissingField) { + assertEquals( + classCompleteField.getPrivateFieldSubClass(), classMissingField.getPrivateFieldSubClass()); + assertEquals(classCompleteField.getPrivateMap(), classMissingField.getPrivateMap()); + assertEquals(classCompleteField.getPrivateList(), classMissingField.getPrivateList()); + assertEquals(classCompleteField.getPrivateString(), classMissingField.getPrivateString()); + assertEquals(classCompleteField.getPrivateInt(), classMissingField.getPrivateInt()); + } +}