Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.apache.fory.resolver;

import org.apache.fory.memory.MemoryBuffer;

/**
* ClassInfoSerializer provides a mechanism to customize the recording of the ClassInfo
* default implementation uses a short value, however when the class registration needs to be customized
* this can be set of the ClassResolver to handle storing the ClassInfo is a customized manner
*/
public interface ClassInfoSerializer {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will introduce another indirection, and incur virtual method call. The performance will degrade. Could you share some detais what will you implement using this ClassInfoSerializer. Maybe we could just merge the funtions of subclass of ClassInfoSerializer into class resolver

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the PR description and the unit test show examples of how this would be used. Basically we maintain a large mapping of identifiers to classes, when an object is deserialized we want to control what ID is mapped to what class. I asked about this on slack a couple months ago, and you said that we cannot customize the class resolve since it final and you suggest instead to raise a PR that supports customized mapping logic. the intent of this feature is for it to be customizable by the calling code not something that is predefined within the class resolver

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fory support register class by name. How about register class by uuid as name? Would this work for you? Your extension is in critical path. We are trying to keep callstack on this be as simple as we can. And you intercept whole class info serialization. Your class schema can't change anymore. The type forward/backward compatibility is implementes in current write/read classinfo method.

Copy link
Author

@mattjubb mattjubb Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that would require registering all classes upfront which means loading the .class, for large amounts of classes that is a very large upfront cost, lets say we have 10,000 classes and we only want to deserialize a single object with a single class - that would mean loading all 10,000 .class to just serialize a single object. if we could provide a fully qualified name (fqn) which could then be loaded when required via Class.forName then registering by name would probably meet our needs

/**
* Writes the provided ClassInfo into the buffer in a customized manner which is consistent
* with the readClassInfo method
*/
void writeClassInfo(ClassResolver classResolver, MemoryBuffer buffer, ClassInfo classInfo);

/**
* Reads the ClassInfo from the provided buffer in a manner which is consistent with writeClassInfo
*/
ClassInfo readClassInfo(ClassResolver classResolver, MemoryBuffer buffer);
}
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ public class ClassResolver extends TypeResolver {
private short innerEndClassId;
private final ShimDispatcher shimDispatcher;

// customized ClassId serialization mechanism
private ClassInfoSerializer classInfoSerializer;

public ClassResolver(Fory fory) {
super(fory);
this.fory = fory;
Expand Down Expand Up @@ -1361,23 +1364,25 @@ public void writeClassAndUpdateCache(MemoryBuffer buffer, Class<?> cls) {
/** Write classname for java serialization. */
@Override
public void writeClassInfo(MemoryBuffer buffer, ClassInfo classInfo) {
if (metaContextShareEnabled) {
// FIXME(chaokunyang) Register class but not register serializer can't be used with
// meta share mode, because no class def are sent to peer.
writeClassInfoWithMetaShare(buffer, classInfo);
} else {
if (classInfo.classId == NO_CLASS_ID) { // no class id provided.
// use classname
// if it's null, it's a bug.
assert classInfo.namespaceBytes != null;
metaStringResolver.writeMetaStringBytesWithFlag(buffer, classInfo.namespaceBytes);
assert classInfo.typeNameBytes != null;
metaStringResolver.writeMetaStringBytes(buffer, classInfo.typeNameBytes);
if (classInfoSerializer != null) {
classInfoSerializer.writeClassInfo(this, buffer, classInfo);
} else if (metaContextShareEnabled) {
// FIXME(chaokunyang) Register class but not register serializer can't be used with
// meta share mode, because no class def are sent to peer.
writeClassInfoWithMetaShare(buffer, classInfo);
} else {
// use classId
buffer.writeVarUint32(classInfo.classId << 1);
if (classInfo.classId == NO_CLASS_ID) { // no class id provided.
// use classname
// if it's null, it's a bug.
assert classInfo.namespaceBytes != null;
metaStringResolver.writeMetaStringBytesWithFlag(buffer, classInfo.namespaceBytes);
assert classInfo.typeNameBytes != null;
metaStringResolver.writeMetaStringBytes(buffer, classInfo.typeNameBytes);
} else {
// use classId
buffer.writeVarUint32(classInfo.classId << 1);
}
}
}
}

public void writeClassInfoWithMetaShare(MemoryBuffer buffer, ClassInfo classInfo) {
Expand Down Expand Up @@ -1483,21 +1488,25 @@ public void writeClassInternal(MemoryBuffer buffer, Class<?> cls) {
}

public void writeClassInternal(MemoryBuffer buffer, ClassInfo classInfo) {
short classId = classInfo.classId;
if (classId == REPLACE_STUB_ID) {
// clear class id to avoid replaced class written as
// ReplaceResolveSerializer.ReplaceStub
classInfo.classId = NO_CLASS_ID;
}
if (classInfo.classId != NO_CLASS_ID) {
buffer.writeVarUint32(classInfo.classId << 1);
} else {
// let the lowermost bit of next byte be set, so the deserialization can know
// whether need to read class by name in advance
metaStringResolver.writeMetaStringBytesWithFlag(buffer, classInfo.namespaceBytes);
metaStringResolver.writeMetaStringBytes(buffer, classInfo.typeNameBytes);
}
classInfo.classId = classId;
short classId = classInfo.classId;
if(classInfoSerializer != null) {
classInfoSerializer.writeClassInfo(this, buffer, classInfo);
} else {
if (classId == REPLACE_STUB_ID) {
// clear class id to avoid replaced class written as
// ReplaceResolveSerializer.ReplaceStub
classInfo.classId = NO_CLASS_ID;
}
if (classInfo.classId != NO_CLASS_ID) {
buffer.writeVarUint32(classInfo.classId << 1);
} else {
// let the lowermost bit of next byte be set, so the deserialization can know
// whether need to read class by name in advance
metaStringResolver.writeMetaStringBytesWithFlag(buffer, classInfo.namespaceBytes);
metaStringResolver.writeMetaStringBytes(buffer, classInfo.typeNameBytes);
}
}
classInfo.classId = classId;
}

/**
Expand All @@ -1506,16 +1515,21 @@ public void writeClassInternal(MemoryBuffer buffer, ClassInfo classInfo) {
* #readClassInfo(MemoryBuffer, ClassInfoHolder)} should be invoked.
*/
public Class<?> readClassInternal(MemoryBuffer buffer) {
int header = buffer.readVarUint32Small14();
final ClassInfo classInfo;
if ((header & 0b1) != 0) {
// let the lowermost bit of next byte be set, so the deserialization can know
// whether need to read class by name in advance
MetaStringBytes packageBytes = metaStringResolver.readMetaStringBytesWithFlag(buffer, header);
MetaStringBytes simpleClassNameBytes = metaStringResolver.readMetaStringBytes(buffer);
classInfo = loadBytesToClassInfo(packageBytes, simpleClassNameBytes);

if(classInfoSerializer != null) {
classInfo = classInfoSerializer.readClassInfo(this, buffer);
} else {
classInfo = registeredId2ClassInfo[(short) (header >> 1)];
int header = buffer.readVarUint32Small14();
if ((header & 0b1) != 0) {
// let the lowermost bit of next byte be set, so the deserialization can know
// whether need to read class by name in advance
MetaStringBytes packageBytes = metaStringResolver.readMetaStringBytesWithFlag(buffer, header);
MetaStringBytes simpleClassNameBytes = metaStringResolver.readMetaStringBytes(buffer);
classInfo = loadBytesToClassInfo(packageBytes, simpleClassNameBytes);
} else {
classInfo = registeredId2ClassInfo[(short) (header >> 1)];
}
}
final Class<?> cls = classInfo.cls;
currentReadClass = cls;
Expand All @@ -1530,16 +1544,24 @@ public ClassInfo readClassInfo(MemoryBuffer buffer) {
if (metaContextShareEnabled) {
return readSharedClassMeta(buffer, fory.getSerializationContext().getMetaContext());
}
int header = buffer.readVarUint32Small14();
ClassInfo classInfo;
if ((header & 0b1) != 0) {
classInfo = readClassInfoFromBytes(buffer, classInfoCache, header);
classInfoCache = classInfo;
} else {
classInfo = getOrUpdateClassInfo((short) (header >> 1));
else {
ClassInfo classInfo;
if(classInfoSerializer != null) {
classInfo = classInfoSerializer.readClassInfo(this, buffer);
classInfoCache = classInfo;
} else {
int header = buffer.readVarUint32Small14();
if ((header & 0b1) != 0) {
classInfo = readClassInfoFromBytes(buffer, classInfoCache, header);
classInfoCache = classInfo;
} else {
classInfo = getOrUpdateClassInfo((short) (header >> 1));
}
}

currentReadClass = classInfo.cls;
return classInfo;
}
currentReadClass = classInfo.cls;
return classInfo;
}

/**
Expand All @@ -1549,7 +1571,9 @@ public ClassInfo readClassInfo(MemoryBuffer buffer) {
@CodegenInvoke
@Override
public ClassInfo readClassInfo(MemoryBuffer buffer, ClassInfo classInfoCache) {
if (metaContextShareEnabled) {
if(classInfoSerializer != null) {
return classInfoSerializer.readClassInfo(this, buffer);
} else if (metaContextShareEnabled) {
return readSharedClassMeta(buffer, fory.getSerializationContext().getMetaContext());
}
int header = buffer.readVarUint32Small14();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package org.apache.fory.serializer;


import lombok.Data;
import org.apache.fory.Fory;
import org.apache.fory.ForyTestBase;
import org.apache.fory.config.Language;
import org.apache.fory.memory.MemoryBuffer;
import org.apache.fory.resolver.ClassInfo;
import org.apache.fory.resolver.ClassInfoSerializer;
import org.apache.fory.resolver.ClassResolver;
import org.testng.Assert;
import org.testng.annotations.Test;

import java.io.IOException;
import java.io.Serializable;
import java.util.UUID;

public class ClassInfoSerializerTest extends ForyTestBase {

@Data
public static class JavaCustomClass implements Serializable {
public String name;
public transient int age;
public static final UUID UUID = new UUID(123L, 456L);

public JavaCustomClass(String name, int age) {
this.name = name;
this.age = age;
}

private void writeObject(java.io.ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeInt(age);
}

private void readObject(java.io.ObjectInputStream s) throws Exception {
s.defaultReadObject();
this.age = s.readInt();
}
}

@Data
public static class CustomClass {
public String name;
public static final UUID UUID = new UUID(444L, 555L);

public CustomClass(String name) {
this.name = name;
}
}


static class Mapper implements ClassInfoSerializer {

@Override
public void writeClassInfo(ClassResolver classResolver, MemoryBuffer buffer, ClassInfo classInfo) {
UUID uuid;

if(classInfo.getCls().equals(JavaCustomClass.class))
uuid = JavaCustomClass.UUID;
else if(classInfo.getCls().equals(CustomClass.class))
uuid = CustomClass.UUID;
else
throw new RuntimeException("unknown class");

buffer.writeVarUint64(uuid.getLeastSignificantBits());
buffer.writeVarUint64(uuid.getMostSignificantBits());
}

@Override
public ClassInfo readClassInfo(ClassResolver classResolver, MemoryBuffer buffer) {
long least = buffer.readVarUint64();
long most = buffer.readVarUint64();
UUID uuid = new UUID(most, least);

if(uuid.equals(JavaCustomClass.UUID))
return classResolver.getClassInfo(JavaCustomClass.class);
else if(uuid.equals(CustomClass.UUID))
return classResolver.getClassInfo(CustomClass.class);
else
throw new RuntimeException("unknown class");
}
}

@Test
public void testJavaObject() {
Fory fory =
Fory.builder()
.withLanguage(Language.JAVA)
.withRefTracking(false)
.requireClassRegistration(false)
.build();

fory.getClassResolver().setClassMapper(new Mapper());

JavaCustomClass deser = serDe(fory, new JavaCustomClass("bob", 10));
Assert.assertEquals(deser.name, "bob");
Assert.assertEquals(deser.age, 10);
}


@Test
public void testObject() {
Fory fory = Fory.builder()
.requireClassRegistration(false)
.build();

fory.getClassResolver().setClassMapper(new Mapper());

CustomClass deser = serDe(fory, new CustomClass("mike"));
Assert.assertEquals(deser.name, "mike");
}

}

Loading