Skip to content

Commit 044efb8

Browse files
authored
feat(java): support type converters for comaptible mode (#2641)
## Why? <!-- Describe the purpose of this PR. --> ## What does this PR do? Support type converters for comaptible mode, so the deserialization peer can have different type for fields, and fory can convert to target type instead of ignoring it. ## Related issues Closes #2636 #1870 ## Does this PR introduce any user-facing change? <!-- If any user-facing interface changes, please [open an issue](https://github.com/apache/fory/issues/new/choose) describing the need to do so and update the document if necessary. Delete section if not applicable. --> - [ ] Does this PR introduce any public API change? - [ ] Does this PR introduce any binary protocol compatibility change? ## Benchmark <!-- When the PR has an impact on performance (if you don't know whether the PR will have an impact on performance, you can submit the PR first, and if it will have impact on performance, the code reviewer will explain it), be sure to attach a benchmark data here. Delete section if not applicable. -->
1 parent e4fe9fc commit 044efb8

File tree

12 files changed

+1004
-22
lines changed

12 files changed

+1004
-22
lines changed

java/fory-core/src/main/java/org/apache/fory/builder/MetaSharedCodecBuilder.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import static org.apache.fory.type.TypeUtils.OBJECT_TYPE;
2424
import static org.apache.fory.type.TypeUtils.STRING_TYPE;
2525

26+
import java.lang.reflect.Field;
2627
import java.lang.reflect.Member;
2728
import java.util.Collection;
2829
import java.util.Map;
@@ -44,7 +45,9 @@
4445
import org.apache.fory.serializer.ObjectSerializer;
4546
import org.apache.fory.serializer.Serializer;
4647
import org.apache.fory.serializer.Serializers;
48+
import org.apache.fory.serializer.converter.FieldConverter;
4749
import org.apache.fory.type.Descriptor;
50+
import org.apache.fory.type.DescriptorBuilder;
4851
import org.apache.fory.type.DescriptorGrouper;
4952
import org.apache.fory.util.DefaultValueUtils;
5053
import org.apache.fory.util.ExceptionUtils;
@@ -221,11 +224,25 @@ protected Expression createRecord(SortedMap<Integer, Expression> recordComponent
221224
@Override
222225
protected Expression setFieldValue(Expression bean, Descriptor descriptor, Expression value) {
223226
if (descriptor.getField() == null) {
227+
FieldConverter<?> converter = descriptor.getFieldConverter();
228+
if (converter != null) {
229+
Field field = converter.getField();
230+
StaticInvoke converted =
231+
new StaticInvoke(
232+
converter.getClass(), "convertFrom", TypeRef.of(field.getType()), value);
233+
Descriptor newDesc =
234+
new DescriptorBuilder(descriptor)
235+
.field(field)
236+
.type(field.getType())
237+
.typeRef(TypeRef.of(field.getType()))
238+
.build();
239+
return super.setFieldValue(bean, newDesc, converted);
240+
}
224241
// Field doesn't exist in current class, skip set this field value.
225242
// Note that the field value shouldn't be an inlined value, otherwise field value read may
226243
// be ignored.
227244
// Add an ignored call here to make expression type to void.
228-
return new Expression.StaticInvoke(ExceptionUtils.class, "ignore", value);
245+
return new StaticInvoke(ExceptionUtils.class, "ignore", value);
229246
}
230247
return super.setFieldValue(bean, descriptor, value);
231248
}

java/fory-core/src/main/java/org/apache/fory/meta/ClassDef.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
import org.apache.fory.resolver.XtypeResolver;
5959
import org.apache.fory.serializer.CompatibleSerializer;
6060
import org.apache.fory.serializer.NonexistentClass;
61+
import org.apache.fory.serializer.converter.FieldConverter;
62+
import org.apache.fory.serializer.converter.FieldConverters;
6163
import org.apache.fory.type.Descriptor;
6264
import org.apache.fory.type.FinalObjectTypeStub;
6365
import org.apache.fory.type.GenericType;
@@ -276,6 +278,11 @@ public List<Descriptor> getDescriptors(TypeResolver resolver, Class<?> cls) {
276278
descriptor = descriptor.copyWithTypeName(newDesc.getTypeName());
277279
descriptors.add(descriptor);
278280
} else {
281+
FieldConverter<?> converter =
282+
FieldConverters.getConverter(rawType, descriptor.getField());
283+
if (converter != null) {
284+
newDesc.setFieldConverter(converter);
285+
}
279286
descriptors.add(newDesc);
280287
}
281288
} else {

java/fory-core/src/main/java/org/apache/fory/reflect/FieldAccessor.java

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,15 @@
2323
import java.lang.invoke.MethodHandles;
2424
import java.lang.reflect.Field;
2525
import java.lang.reflect.Method;
26+
import java.util.concurrent.ConcurrentHashMap;
27+
import java.util.concurrent.ConcurrentMap;
2628
import java.util.function.Function;
2729
import java.util.function.Predicate;
2830
import java.util.function.ToDoubleFunction;
2931
import java.util.function.ToIntFunction;
3032
import java.util.function.ToLongFunction;
33+
import org.apache.fory.collection.ClassValueCache;
34+
import org.apache.fory.collection.Tuple2;
3135
import org.apache.fory.memory.Platform;
3236
import org.apache.fory.type.TypeUtils;
3337
import org.apache.fory.util.GraalvmSupport;
@@ -494,20 +498,39 @@ public Object get(Object obj) {
494498
}
495499

496500
static class GeneratedAccessor extends FieldAccessor {
501+
private static final ClassValueCache<ConcurrentMap<String, Tuple2<MethodHandle, MethodHandle>>>
502+
cache = ClassValueCache.newClassKeyCache(8);
503+
497504
private final MethodHandle getter;
498505
private final MethodHandle setter;
499506

500507
protected GeneratedAccessor(Field field) {
501508
super(field, -1);
502-
MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(field.getDeclaringClass());
509+
ConcurrentMap<String, Tuple2<MethodHandle, MethodHandle>> map;
503510
try {
504-
this.getter =
505-
lookup.findGetter(field.getDeclaringClass(), field.getName(), field.getType());
506-
this.setter =
507-
lookup.findSetter(field.getDeclaringClass(), field.getName(), field.getType());
508-
} catch (IllegalAccessException | NoSuchFieldException ex) {
509-
throw new RuntimeException(ex);
511+
map = cache.get(field.getDeclaringClass(), ConcurrentHashMap::new);
512+
} catch (Exception e) {
513+
throw new RuntimeException(e);
510514
}
515+
MethodHandles.Lookup lookup = _JDKAccess._trustedLookup(field.getDeclaringClass());
516+
Tuple2<MethodHandle, MethodHandle> tuple2 =
517+
map.computeIfAbsent(
518+
field.getName(),
519+
k -> {
520+
try {
521+
MethodHandle getter =
522+
lookup.findGetter(
523+
field.getDeclaringClass(), field.getName(), field.getType());
524+
MethodHandle setter =
525+
lookup.findSetter(
526+
field.getDeclaringClass(), field.getName(), field.getType());
527+
return Tuple2.of(getter, setter);
528+
} catch (IllegalAccessException | NoSuchFieldException ex) {
529+
throw new RuntimeException(ex);
530+
}
531+
});
532+
getter = tuple2.f0;
533+
setter = tuple2.f1;
511534
}
512535

513536
@Override

java/fory-core/src/main/java/org/apache/fory/reflect/ReflectionUtils.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.Arrays;
3535
import java.util.Collection;
3636
import java.util.Collections;
37+
import java.util.Comparator;
3738
import java.util.HashSet;
3839
import java.util.LinkedHashMap;
3940
import java.util.LinkedHashSet;
@@ -402,6 +403,12 @@ public static List<Field> getFieldsWithoutSuperClasses(Class<?> cls, Set<Class<?
402403
return fields;
403404
}
404405

406+
public static List<Field> getSortedFields(Class<?> cls, boolean searchParent) {
407+
List<Field> fields = getFields(cls, searchParent);
408+
fields.sort(Comparator.comparing(f -> f.getName() + f.getDeclaringClass()));
409+
return fields;
410+
}
411+
405412
/** Get fields values from provided object. */
406413
public static List<Object> getFieldValues(Collection<Field> fields, Object o) {
407414
List<Object> results = new ArrayList<>(fields.size());

java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.apache.fory.resolver.ClassResolver;
4444
import org.apache.fory.resolver.RefResolver;
4545
import org.apache.fory.resolver.TypeResolver;
46+
import org.apache.fory.serializer.converter.FieldConverter;
4647
import org.apache.fory.type.Descriptor;
4748
import org.apache.fory.type.DescriptorGrouper;
4849
import org.apache.fory.type.FinalObjectTypeStub;
@@ -990,6 +991,7 @@ public static class InternalFieldInfo {
990991
protected final short classId;
991992
protected final String qualifiedFieldName;
992993
protected final FieldAccessor fieldAccessor;
994+
protected final FieldConverter<?> fieldConverter;
993995
protected boolean nullable;
994996
protected boolean trackingRef;
995997

@@ -998,6 +1000,7 @@ private InternalFieldInfo(Fory fory, Descriptor d, short classId) {
9981000
this.classId = classId;
9991001
this.qualifiedFieldName = d.getDeclaringClass() + "." + d.getName();
10001002
this.fieldAccessor = d.getField() != null ? FieldAccessor.createAccessor(d.getField()) : null;
1003+
fieldConverter = d.getFieldConverter();
10011004
ForyField foryField = d.getForyField();
10021005
nullable = d.isNullable();
10031006
if (fory.trackingRef()) {

java/fory-core/src/main/java/org/apache/fory/serializer/MetaSharedSerializer.java

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -200,15 +200,19 @@ public T read(MemoryBuffer buffer) {
200200
fieldAccessor.putObject(obj, fieldValue);
201201
}
202202
} else {
203-
// Skip the field value from buffer since it doesn't exist in current class
204-
if (skipPrimitiveFieldValueFailed(fory, fieldInfo.classId, buffer)) {
205-
if (fieldInfo.classInfo == null) {
206-
// TODO(chaokunyang) support registered serializer in peer with ref tracking disabled.
207-
fory.readRef(buffer, classInfoHolder);
208-
} else {
209-
AbstractObjectSerializer.readFinalObjectFieldValue(
210-
binding, refResolver, classResolver, fieldInfo, isFinal, buffer);
203+
if (fieldInfo.fieldConverter == null) {
204+
// Skip the field value from buffer since it doesn't exist in current class
205+
if (skipPrimitiveFieldValueFailed(fory, fieldInfo.classId, buffer)) {
206+
if (fieldInfo.classInfo == null) {
207+
// TODO(chaokunyang) support registered serializer in peer with ref tracking disabled.
208+
fory.readRef(buffer, classInfoHolder);
209+
} else {
210+
AbstractObjectSerializer.readFinalObjectFieldValue(
211+
binding, refResolver, classResolver, fieldInfo, isFinal, buffer);
212+
}
211213
}
214+
} else {
215+
compatibleRead(buffer, fieldInfo, isFinal, obj);
212216
}
213217
}
214218
}
@@ -228,20 +232,32 @@ public T read(MemoryBuffer buffer) {
228232
fieldAccessor.putObject(obj, fieldValue);
229233
}
230234
}
235+
return obj;
236+
}
231237

232-
// Set default values for missing fields in Scala case classes
233-
if (hasDefaultValues) {
234-
DefaultValueUtils.setDefaultValues(obj, defaultValueFields);
238+
private void compatibleRead(
239+
MemoryBuffer buffer, FinalTypeField fieldInfo, boolean isFinal, Object obj) {
240+
Object fieldValue;
241+
short classId = fieldInfo.classId;
242+
if (classId >= ClassResolver.PRIMITIVE_BOOLEAN_CLASS_ID
243+
&& classId <= ClassResolver.PRIMITIVE_DOUBLE_CLASS_ID) {
244+
fieldValue = Serializers.readPrimitiveValue(fory, buffer, classId);
245+
} else {
246+
fieldValue =
247+
AbstractObjectSerializer.readFinalObjectFieldValue(
248+
binding, refResolver, classResolver, fieldInfo, isFinal, buffer);
235249
}
236-
237-
return obj;
250+
fieldInfo.fieldConverter.set(obj, fieldValue);
238251
}
239252

240253
private T newInstance() {
241254
if (!hasDefaultValues) {
242255
return newBean();
243256
}
244-
return Platform.newInstance(type);
257+
T obj = Platform.newInstance(type);
258+
// Set default values for missing fields in Scala case classes
259+
DefaultValueUtils.setDefaultValues(obj, defaultValueFields);
260+
return obj;
245261
}
246262

247263
@Override
@@ -284,6 +300,7 @@ private void readFields(MemoryBuffer buffer, Object[] fields) {
284300
binding, refResolver, classResolver, fieldInfo, isFinal, buffer);
285301
}
286302
}
303+
// remapping will handle those extra fields from peers.
287304
fields[counter++] = null;
288305
}
289306
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.fory.serializer.converter;
21+
22+
import java.lang.reflect.Field;
23+
import org.apache.fory.reflect.FieldAccessor;
24+
25+
/**
26+
* Abstract base class for field converters that handle type conversions during
27+
* serialization/deserialization.
28+
*
29+
* <p>Field converters are responsible for converting values from one type to another when setting
30+
* field values. This is particularly useful when dealing with cross-language serialization where
31+
* type mappings may differ between languages, or when handling legacy data with different type
32+
* representations.
33+
*
34+
* <p>Each converter is associated with a specific field through a {@link FieldAccessor}, which
35+
* provides the mechanism to actually set the converted value on the target object.
36+
*
37+
* <p>Example usage:
38+
*
39+
* <pre>{@code
40+
* // Create a converter for an int field
41+
* FieldConverter<Integer> converter = new IntConverter(fieldAccessor);
42+
*
43+
* // Convert a string "123" to integer and set it on the target object
44+
* converter.set(targetObject, "123");
45+
* }</pre>
46+
*
47+
* @param <T> the target type that this converter produces
48+
* @see FieldConverters for factory methods to create specific converter instances
49+
* @see FieldAccessor for the field access mechanism
50+
*/
51+
public abstract class FieldConverter<T> {
52+
private final FieldAccessor fieldAccessor;
53+
54+
/**
55+
* Constructs a new FieldConverter with the specified field accessor.
56+
*
57+
* @param fieldAccessor the field accessor that will be used to set converted values on target
58+
* objects
59+
* @throws IllegalArgumentException if fieldAccessor is null
60+
*/
61+
protected FieldConverter(FieldAccessor fieldAccessor) {
62+
this.fieldAccessor = fieldAccessor;
63+
}
64+
65+
/**
66+
* Converts the given object to the target type.
67+
*
68+
* <p>This method performs the actual type conversion logic. The implementation should handle null
69+
* values appropriately and throw {@link UnsupportedOperationException} for incompatible types.
70+
*
71+
* @param from the object to convert
72+
* @return the converted object of type T
73+
* @throws UnsupportedOperationException if the source type is not compatible with this converter
74+
* @throws NumberFormatException if converting from String to a numeric type and the string is not
75+
* a valid number
76+
* @throws ArithmeticException if the numeric conversion would result in overflow or underflow
77+
*/
78+
public abstract T convert(Object from);
79+
80+
/**
81+
* Converts the given object and sets it on the target object's field.
82+
*
83+
* <p>This is a convenience method that combines conversion and field setting in one operation. It
84+
* first converts the input object using {@link #convert(Object)}, then uses the field accessor to
85+
* set the converted value on the target object.
86+
*
87+
* @param target the target object whose field will be set
88+
* @param from the object to convert and set
89+
* @throws UnsupportedOperationException if the source type is not compatible with this converter
90+
* @throws NumberFormatException if converting from String to a numeric type and the string is not
91+
* a valid number
92+
* @throws ArithmeticException if the numeric conversion would result in overflow or underflow
93+
* @throws IllegalArgumentException if target is null or the field accessor fails
94+
*/
95+
public void set(Object target, Object from) {
96+
T converted = convert(from);
97+
fieldAccessor.set(target, converted);
98+
}
99+
100+
public Field getField() {
101+
return fieldAccessor.getField();
102+
}
103+
}

0 commit comments

Comments
 (0)