Java Serialization Guide
When only java object serialization needed, this mode will have better performance compared to cross-language object graph serialization.
Quick Start
Note that fory creation is not cheap, the fory instances should be reused between serializations instead of creating it everytime. You should keep fory to a static global variable, or instance variable of some singleton object or limited objects.
Fory for single-thread usage:
import java.util.List;
import java.util.Arrays;
import org.apache.fory.*;
import org.apache.fory.config.*;
public class Example {
public static void main(String[] args) {
SomeClass object = new SomeClass();
// Note that Fory instances should be reused between
// multiple serializations of different objects.
Fory fory = Fory.builder().withLanguage(Language.JAVA)
.requireClassRegistration(true)
.build();
// Registering types can reduce class name serialization overhead, but not mandatory.
// If class registration enabled, all custom types must be registered.
fory.register(SomeClass.class);
byte[] bytes = fory.serialize(object);
System.out.println(fory.deserialize(bytes));
}
}
Fory for multiple-thread usage:
import java.util.List;
import java.util.Arrays;
import org.apache.fory.*;
import org.apache.fory.config.*;
public class Example {
public static void main(String[] args) {
SomeClass object = new SomeClass();
// Note that Fory instances should be reused between
// multiple serializations of different objects.
ThreadSafeFory fory = new ThreadLocalFory(classLoader -> {
Fory f = Fory.builder().withLanguage(Language.JAVA)
.withClassLoader(classLoader).build();
f.register(SomeClass.class);
return f;
});
byte[] bytes = fory.serialize(object);
System.out.println(fory.deserialize(bytes));
}
}
Fory instances reuse example:
import java.util.List;
import java.util.Arrays;
import org.apache.fory.*;
import org.apache.fory.config.*;
public class Example {
// reuse fory.
private static final ThreadSafeFory fory = new ThreadLocalFory(classLoader -> {
Fory f = Fory.builder().withLanguage(Language.JAVA)
.withClassLoader(classLoader).build();
f.register(SomeClass.class);
return f;
});
public static void main(String[] args) {
SomeClass object = new SomeClass();
byte[] bytes = fory.serialize(object);
System.out.println(fory.deserialize(bytes));
}
}
ForyBuilder options
Option Name | Description | Default Value |
---|---|---|
timeRefIgnored | Whether to ignore reference tracking of all time types registered in TimeSerializers and subclasses of those types when ref tracking is enabled. If ignored, ref tracking of every time type can be enabled by invoking Fory#registerSerializer(Class, Serializer) . For example, fory.registerSerializer(Date.class, new DateSerializer(fory, true)) . Note that enabling ref tracking should happen before serializer codegen of any types which contain time fields. Otherwise, those fields will still skip ref tracking. | true |
compressInt | Enables or disables int compression for smaller size. | true |
compressLong | Enables or disables long compression for smaller size. | true |
compressString | Enables or disables string compression for smaller size. | false |
classLoader | The classloader should not be updated; Fory caches class metadata. Use LoaderBinding or ThreadSafeFory for classloader updates. | Thread.currentThread().getContextClassLoader() |
compatibleMode | Type forward/backward compatibility config. Also Related to checkClassVersion config. SCHEMA_CONSISTENT : Class schema must be consistent between serialization peer and deserialization peer. COMPATIBLE : Class schema can be different between serialization peer and deserialization peer. They can add/delete fields independently. See more. | CompatibleMode.SCHEMA_CONSISTENT |
checkClassVersion | Determines whether to check the consistency of the class schema. If enabled, Fory checks, writes, and checks consistency using the classVersionHash . It will be automatically disabled when CompatibleMode#COMPATIBLE is enabled. Disabling is not recommended unless you can ensure the class won't evolve. | false |
checkJdkClassSerializable | Enables or disables checking of Serializable interface for classes under java.* . If a class under java.* is not Serializable , Fory will throw an UnsupportedOperationException . | true |
registerGuavaTypes | Whether to pre-register Guava types such as RegularImmutableMap /RegularImmutableList . These types are not public API, but seem pretty stable. | true |
requireClassRegistration | Disabling may allow unknown classes to be deserialized, potentially causing security risks. | true |
suppressClassRegistrationWarnings | Whether to suppress class registration warnings. The warnings can be used for security audit, but may be annoying, this suppression will be enabled by default. | true |
metaShareEnabled | Enables or disables meta share mode. | true if CompatibleMode.Compatible is set, otherwise false. |
scopedMetaShareEnabled | Scoped meta share focuses on a single serialization process. Metadata created or identified during this process is exclusive to it and is not shared with by other serializations. | true if CompatibleMode.Compatible is set, otherwise false. |
metaCompressor | Set a compressor for meta compression. Note that the passed MetaCompressor should be thread-safe. By default, a Deflater based compressor DeflaterMetaCompressor will be used. Users can pass other compressor such as zstd for better compression rate. | DeflaterMetaCompressor |
deserializeNonexistentClass | Enables or disables deserialization/skipping of data for non-existent classes. | true if CompatibleMode.Compatible is set, otherwise false. |
codeGenEnabled | Disabling may result in faster initial serialization but slower subsequent serializations. | true |
asyncCompilationEnabled | If enabled, serialization uses interpreter mode first and switches to JIT serialization after async serializer JIT for a class is finished. | false |
scalaOptimizationEnabled | Enables or disables Scala-specific serialization optimization. | false |
copyRef | When disabled, the copy performance will be better. But fory deep copy will ignore circular and shared reference. Same reference of an object graph will be copied into different objects in one Fory#copy . | true |
serializeEnumByName | When Enabled, fory serialize enum by name instead of ordinal. | false |
Advanced Usage
Fory creation
Single thread fory:
Fory fory = Fory.builder()
.withLanguage(Language.JAVA)
// enable reference tracking for shared/circular reference.
// Disable it will have better performance if no duplicate reference.
.withRefTracking(false)
.withCompatibleMode(CompatibleMode.SCHEMA_CONSISTENT)
// enable type forward/backward compatibility
// disable it for small size and better performance.
// .withCompatibleMode(CompatibleMode.COMPATIBLE)
// enable async multi-threaded compilation.
.withAsyncCompilation(true)
.build();
byte[] bytes = fory.serialize(object);
System.out.println(fory.deserialize(bytes));
Thread-safe fory:
ThreadSafeFory fory = Fory.builder()
.withLanguage(Language.JAVA)
// enable reference tracking for shared/circular reference.
// Disable it will have better performance if no duplicate reference.
.withRefTracking(false)
// compress int for smaller size
// .withIntCompressed(true)
// compress long for smaller size
// .withLongCompressed(true)
.withCompatibleMode(CompatibleMode.SCHEMA_CONSISTENT)
// enable type forward/backward compatibility
// disable it for small size and better performance.
// .withCompatibleMode(CompatibleMode.COMPATIBLE)
// enable async multi-threaded compilation.
.withAsyncCompilation(true)
.buildThreadSafeFory();
byte[] bytes = fory.serialize(object);
System.out.println(fory.deserialize(bytes));
Handling Class Schema Evolution in Serialization
In many systems, the schema of a class used for serialization may change over time. For instance, fields within a class may be added or removed. When serialization and deserialization processes use different versions of jars, the schema of the class being deserialized may differ from the one used during serialization.
By default, Fory serializes objects using the CompatibleMode.SCHEMA_CONSISTENT
mode. This mode assumes that the
deserialization process uses the same class schema as the serialization process, minimizing payload overhead.
However, if there is a schema inconsistency, deserialization will fail.
If the schema is expected to change, to make deserialization succeed, i.e. schema forward/backward compatibility.
Users must configure Fory to use CompatibleMode.COMPATIBLE
. This can be done using the
ForyBuilder#withCompatibleMode(CompatibleMode.COMPATIBLE)
method.
In this compatible mode, deserialization can handle schema changes such as missing or extra fields, allowing it to
succeed even when the serialization and deserialization processes have different class schemas.
Here is an example of creating Fory to support schema evolution:
Fory fory = Fory.builder()
.withCompatibleMode(CompatibleMode.COMPATIBLE)
.build();
byte[] bytes = fory.serialize(object);
System.out.println(fory.deserialize(bytes));
This compatible mode involves serializing class metadata into the serialized output. Despite Fory's use of sophisticated compression techniques to minimize overhead, there is still some additional space cost associated with class metadata.
To further reduce metadata costs, Fory introduces a class metadata sharing mechanism, which allows the metadata to be sent to the deserialization process only once. For more details, please refer to the Meta Sharing specification.
Compression
ForyBuilder#withIntCompressed
/ForyBuilder#withLongCompressed
can be used to compress int/long for smaller size.
Normally compress int is enough.
Both compression are enabled by default, if the serialized is not important, for example, you use flatbuffers for serialization before, which doesn't compress anything, then you should disable compression. If your data are all numbers, the compression may bring 80% performance regression.
For int compression, fory use 1~5 bytes for encoding. First bit in every byte indicate whether has next byte. if first bit is set, then next byte will be read util first bit of next byte is unset.
For long compression, fory support two encoding:
- Fory SLI(Small long as int) Encoding (used by default):
- If long is in
[-1073741824, 1073741823]
, encode as 4 bytes int:| little-endian: ((int) value) << 1 |
- Otherwise write as 9 bytes:
| 0b1 | little-endian 8bytes long |
- If long is in
- Fory PVL(Progressive Variable-length Long) Encoding:
- First bit in every byte indicate whether has next byte. if first bit is set, then next byte will be read util first bit of next byte is unset.
- Negative number will be converted to positive number by
(v << 1) ^ (v >> 63)
to reduce cost of small negative numbers.
If a number are long
type, it can't be represented by smaller bytes mostly, the compression won't get good enough
result,
not worthy compared to performance cost. Maybe you should try to disable long compression if you find it didn't bring
much
space savings.
Object deep copy
Deep copy example:
Fory fory = Fory.builder().withRefCopy(true).build();
SomeClass a = xxx;
SomeClass copied = fory.copy(a);
Make fory deep copy ignore circular and shared reference, this deep copy mode will ignore circular and shared reference.
Same reference of an object graph will be copied into different objects in one Fory#copy
.
Fory fory = Fory.builder().withRefCopy(false).build();
SomeClass a = xxx;
SomeClass copied = fory.copy(a);
Implement a customized serializer
In some cases, you may want to implement a serializer for your type, especially some class customize serialization by
JDK writeObject/writeReplace/readObject/readResolve
, which is very inefficient. For example, if you don't want
following Foo#writeObject
got invoked, you can take following FooSerializer
as an example:
class Foo {
public long f1;
private void writeObject(ObjectOutputStream s) throws IOException {
System.out.println(f1);
s.defaultWriteObject();
}
}
class FooSerializer extends Serializer<Foo> {
public FooSerializer(Fory fory) {
super(fory, Foo.class);
}
@Override
public void write(MemoryBuffer buffer, Foo value) {
buffer.writeInt64(value.f1);
}
@Override
public Foo read(MemoryBuffer buffer) {
Foo foo = new Foo();
foo.f1 = buffer.readInt64();
return foo;
}
}
Register serializer:
Fory fory = getFory();
fory.registerSerializer(Foo.class, new FooSerializer(fory));
Implement Collection Serializer
Similar to maps, when implementing a serializer for a custom Collection type, you must extend CollectionSerializer
or AbstractCollectionSerializer
.
The key difference between these two is that AbstractCollectionSerializer
can serialize a class which has a collection-like structure but is not a java Collection subtype.
For collection serializer, this is a special parameter supportCodegenHook
needs be configured:
-
When
true
:- Enables optimized access to collection elements and JIT compilation for better performance
- Direct serialization invocation and inline for map key-value items without dynamic serializer dispatch cost.
- Better performance for standard collection types
- Recommended for most collections
-
When
false
:- Uses interfaced-based element access and dynamic serializer dispatch for elements, which have higer cost
- More flexible for custom collection types
- Required when collection has special serialization needs
- Handles complex collection implementations
Implement Collection Serializer with JIT support
When implementing a Collection serializer with JIT support, you can leverage Fory's existing binary format and collection serialization infrastructure. The key is to properly implement the onCollectionWrite
and newCollection
methods to handle metadata while letting Fory handle the element serialization.
Here's an example:
public class CustomCollectionSerializer<T extends Collection> extends CollectionSerializer<T> {
public CustomCollectionSerializer(Fory fory, Class<T> cls) {
// supportCodegenHook controls whether to use JIT compilation
super(fory, cls, true);
}
@Override
public Collection onCollectionWrite(MemoryBuffer buffer, T value) {
// Write collection size
buffer.writeVarUint32Small7(value.size());
// Write any additional collection metadata
return value;
}
@Override
public Collection newCollection(MemoryBuffer buffer) {
// Create new collection instance
Collection collection = super.newCollection(buffer);
// Read and set collection size
int numElements = getAndClearNumElements();
setNumElements(numElements);
return collection;
}
}
Note that please invoke setNumElements
when implementing newCollection
to let fory know how many elements to deserialize.
Implement a totally-customzied Collection Serializer without JIT
Sometimes you need to serialize a collection type that uses primitive arrays or has special requirements.
In such cases, you can implement a serializer with JIT disabled and directly override the write
and read
methods.
This approach:
- Gives you full control over serialization
- Works well with primitive arrays
- Bypasses collection iteration overhead
- Allows direct memory access
Here's an example of a custom integer list backed by a primitive array:
class IntList extends AbstractCollection<Integer> {
private final int[] elements;
private final int size;
public IntList(int size) {
this.elements = new int[size];
this.size = size;
}
public IntList(int[] elements, int size) {
this.elements = elements;
this.size = size;
}
@Override
public Iterator<Integer> iterator() {
return new Iterator<Integer>() {
private int index = 0;
@Override
public boolean hasNext() {
return index < size;
}
@Override
public Integer next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return elements[index++];
}
};
}
@Override
public int size() {
return size;
}
public int get(int index) {
if (index >= size) {
throw new IndexOutOfBoundsException();
}
return elements[index];
}
public void set(int index, int value) {
if (index >= size) {
throw new IndexOutOfBoundsException();
}
elements[index] = value;
}
public int[] getElements() {
return elements;
}
}
class IntListSerializer extends AbstractCollectionSerializer<IntList> {
public IntListSerializer(Fory fory) {
// Disable JIT since we're handling serialization directly
super(fory, IntList.class, false);
}
@Override
public void write(MemoryBuffer buffer, IntList value) {
// Write size
buffer.writeVarUint32Small7(value.size());
// Write elements directly as primitive ints
int[] elements = value.getElements();
for (int i = 0; i < value.size(); i++) {
buffer.writeVarInt32(elements[i]);
}
}
@Override
public IntList read(MemoryBuffer buffer) {
// Read size
int size = buffer.readVarUint32Small7();
// Create array and read elements
int[] elements = new int[size];
for (int i = 0; i < size; i++) {
elements[i] = buffer.readVarInt32();
}
return new IntList(elements, size);
}
// These methods are not used when JIT is disabled
@Override
public Collection onCollectionWrite(MemoryBuffer buffer, IntList value) {
throw new UnsupportedOperationException();
}
@Override
public Collection newCollection(MemoryBuffer buffer) {
throw new UnsupportedOperationException();
}
@Override
public IntList onCollectionRead(Collection collection) {
throw new UnsupportedOperationException();
}
}
Key Points:
-
Primitive Array Storage:
- Uses
int[]
for direct storage - Avoids boxing/unboxing overhead
- Provides efficient memory layout
- Enables direct array access
- Uses
-
Direct Serialization:
- Write size first
- Write primitive values directly
- No iteration overhead
- No boxing/unboxing during serialization
-
Direct Deserialization:
- Read size first
- Create primitive array
- Read values directly into array
- Create list with populated array
-
Disabled JIT:
- Set
supportCodegenHook=false
- Override
write
/read
methods - Skip collection view pattern
- Full control over serialization format
- Set
When to Use: this approach is best when:
- Working with primitive types
- Need maximum performance
- Want to minimize memory overhead
- Have special serialization requirements
Usage Example:
// Create and populate list
IntList list = new IntList(3);
list.set(0, 1);
list.set(1, 2);
list.set(2, 3);
// Serialize
byte[] bytes = fory.serialize(list);
// Deserialize
IntList newList = (IntList) fory.deserialize(bytes);
This implementation is particularly efficient for scenarios where:
- You're working exclusively with integers
- Performance is critical
- Memory efficiency is important
- Serialization overhead needs to be minimized
Remember that while this approach gives up some of Fory's optimizations, it can provide better performance for specific use cases involving primitive types and direct array access.
Implement Serializer for Collection-like Types
Sometimes you may want to implement a serializer for a type that behaves like a collection but isn't a standard Java Collection. This section demonstrates how to implement a serializer for such types.
The key principles for collection-like type serialization are:
- Extend
AbstractCollectionSerializer
for custom collection-like types - Enable JIT optimization with
supportCodegenHook
- Provide efficient element access through views
- Maintain proper size tracking
Here's an example:
class CustomCollectionLike {
private final Object[] elements;
private final int size;
public CustomCollectionLike(int size) {
this.elements = new Object[size];
this.size = size;
}
// Constructor for wrapping existing array
public CustomCollectionLike(Object[] elements, int size) {
this.elements = elements;
this.size = size;
}
public Object get(int index) {
if (index >= size) {
throw new IndexOutOfBoundsException();
}
return elements[index];
}
public int size() {
return size;
}
public Object[] getElements() {
return elements;
}
}
// A view class that extends AbstractCollection for simpler implementation
class CollectionView extends AbstractCollection<Object> {
private final Object[] elements;
private final int size;
private int writeIndex;
// Constructor for serialization (wrapping existing array)
public CollectionView(CustomCollectionLike collection) {
this.elements = collection.getElements();
this.size = collection.size();
}
// Constructor for deserialization
public CollectionView(int size) {
this.size = size;
this.elements = new Object[size];
}
@Override
public Iterator<Object> iterator() {
return new Iterator<Object>() {
private int index = 0;
@Override
public boolean hasNext() {
return index < size;
}
@Override
public Object next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return elements[index++];
}
};
}
@Override
public boolean add(Object element) {
if (writeIndex >= size) {
throw new IllegalStateException("Collection is full");
}
elements[writeIndex++] = element;
return true;
}
@Override
public int size() {
return size;
}
public Object[] getElements() {
return elements;
}
}
class CustomCollectionSerializer extends AbstractCollectionSerializer<CustomCollectionLike> {
public CustomCollectionSerializer(Fory fory) {
super(fory, CustomCollectionLike.class, true);
}
@Override
public Collection onCollectionWrite(MemoryBuffer buffer, CustomCollectionLike value) {
buffer.writeVarUint32Small7(value.size());
return new CollectionView(value);
}
@Override
public Collection newCollection(MemoryBuffer buffer) {
int numElements = buffer.readVarUint32Small7();
setNumElements(numElements);
return new CollectionView(numElements);
}
@Override
public CustomCollectionLike onCollectionRead(Collection collection) {
CollectionView view = (CollectionView) collection;
return new CustomCollectionLike(view.getElements(), view.size());
}
}
Key takeways:
-
Collection Structure:
- Array-based storage for elements
- Fixed size after creation
- Direct element access
- Size tracking
-
View Implementation:
- Extends
AbstractCollection
for simplicity - Provides iterator for element access
- Implements
add()
for deserialization - Shares array reference with original type
- Extends
-
Serializer Features:
- Uses
supportCodegenHook=true
for JIT optimization - Shares array references when possible
- Maintains proper size tracking
- Uses view pattern for serialization
- Uses
-
Performance Aspects:
- Direct array access
- Minimal object creation
- Array sharing between instances
- Efficient iteration
Note that this implementation provides better performance at the cost of flexibility. Consider your specific use case when choosing this approach.
Implement Map Serializer
When implementing a serializer for a custom Map type, you must extend MapSerializer
or AbstractMapSerializer
. The key difference between these two is that AbstractMapSerializer
can serialize a class which has a map-like structure but is not a java Map subtype.
Similiar to collection serializer, this is a special parameter supportCodegenHook
needs be configured:
-
When
true
:- Enables optimized access to map elements and JIT compilation for better performance
- Direct serialization invocation and inline for map key-value items without dynamic serializer dispatch cost.
- Better performance for standard map types
- Recommended for most maps
-
When
false
:- Uses interfaced-based element access and dynamic serializer dispatch for elements, which have higer cost
- More flexible for custom map types
- Required when map has special serialization needs
- Handles complex map implementations
Implement Map Serializer with JIT support
When implementing a Map serializer with JIT support, you can leverage Fory's existing chunk-based binary format and map serialization infrastructure. The key is to properly implement the onMapWrite
and newMap
methods to handle metadata while letting Fory handle the map key-value serialization.
Here's an example of implementing a custom map serializer:
public class CustomMapSerializer<T extends Map> extends MapSerializer<T> {
public CustomMapSerializer(Fory fory, Class<T> cls) {
// supportCodegenHook is a critical parameter that determines serialization behavior
super(fory, cls, true);
}
@Override
public Map onMapWrite(MemoryBuffer buffer, T value) {
// Write map size
buffer.writeVarUint32Small7(value.size());
// Write any additional map metadata here
return value;
}
@Override
public Map newMap(MemoryBuffer buffer) {
// Read map size
int numElements = buffer.readVarUint32Small7();
setNumElements(numElements);
// Create and return new map instance
T map = (T) new HashMap(numElements);
fory.getRefResolver().reference(map);
return map;
}
}
Note that please invoke setNumElements
when implementing newMap
to let fory know how many elements to deserialize.