Interface JGroupsRaftCustomMarshaller<T>
- Type Parameters:
T- The type of object this serializer handles
Each serializer implementation handles serialization and deserialization for exactly one concrete class. All operations
submitted through JGroupsRaft require proper serialization to ensure data consistency across the cluster.
Basic Usage
To serialize a custom type, implement this interface and register it when building your JGroupsRaft instance:
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// getters...
}
public class PersonMarshaller implements JGroupsRaftCustomMarshaller<Person> {
@Override
public void write(SerializationContextWrite ctx, Person person) {
ctx.writeUTF(person.getName());
ctx.writeInt(person.getAge());
}
@Override
public Person read(SerializationContextRead ctx, byte version) {
String name = ctx.readUTF();
int age = ctx.readInt();
return new Person(name, age);
}
@Override
public Class<Person> javaClass() {
return Person.class;
}
@Override
public int type() {
return 1001; // Choose a unique type ID
}
@Override
public byte version() {
return 1; // Current version of this serializer
}
}
// Register the serializer:
JGroupsRaft<MyStateMachine> raft = JGroupsRaft.builder(stateMachine, MyStateMachine.class)
.registerMarshaller(new PersonMarshaller())
.build();
Type IDs
Each serializer must return a unique type ID via type(). The type ID is utilized to uniquely identify the
entry in the byte stream.
Important guidelines for type IDs:
- Choose IDs starting from 1000 - IDs below 1000 are reserved for internal use, an exception will be thrown.
- Never reuse a type ID - Once a type ID is used in production, it must remain permanently associated with that type.
- Never change a type ID - Changing would break compatibility with existing persisted data
- Document your type IDs - Maintain a documentation of type IDs in your codebase to avoid unexpected changes.
// Good practice: Document your type IDs
public class TypeIds {
public static final int PERSON = 1001;
public static final int ADDRESS = 1002;
public static final int ORDER = 1003;
// ... add new IDs as needed
}
Versioning and Evolution
The version() method returns the current version of your serializer's wire format. Increment this version when
you need to change the serialization format while maintaining backward compatibility. The version byte is passed to
read(SerializationContextRead, byte), allowing you to handle multiple format versions during deserialization:
public class PersonMarshaller implements JGroupsRaftCustomMarshaller<Person> {
@Override
public void write(SerializationContextWrite ctx, Person person) {
// Version 2 format: added email field
ctx.writeUTF(person.getName());
ctx.writeInt(person.getAge());
ctx.writeUTF(person.getEmail()); // NEW in version 2
}
@Override
public Person read(SerializationContextRead ctx, byte version) {
String name = ctx.readUTF();
int age = ctx.readInt();
// Handle both version 1 and version 2
String email = version >= 2 ? ctx.readUTF() : "unknown@example.com";
return new Person(name, age, email);
}
@Override
public byte version() {
return 2; // Incremented from 1
}
// ... other methods
}
Forward Compatibility
The framework automatically handles forward compatibility (old code reading new data). When an old serializer encounters data from a newer version, it reads what it understands and the framework automatically skips unknown trailing fields.
This works automatically - you don't need to do anything special. Just make sure to increment version()
when adding new fields.
Nested Objects
You can serialize nested objects using SerializationContextWrite.writeObject(Object) and
SerializationContextRead.readObject(). The nested object's serializer will be automatically invoked:
public class Order {
private final Person customer;
private final List<String> items;
private final double total;
}
public class OrderMarshaller implements JGroupsRaftCustomMarshaller<Order> {
@Override
public void write(SerializationContextWrite ctx, Order order) {
ctx.writeObject(order.getCustomer()); // Delegates to PersonMarshaller
ctx.writeInt(order.getItems().size());
for (String item : order.getItems()) {
ctx.writeUTF(item);
}
ctx.writeDouble(order.getTotal());
}
@Override
public Order read(SerializationContextRead ctx, byte version) {
Person customer = ctx.readObject(); // Delegates to PersonMarshaller
int itemCount = ctx.readInt();
List<String> items = new ArrayList<>(itemCount);
for (int i = 0; i < itemCount; i++) {
items.add(ctx.readUTF());
}
double total = ctx.readDouble();
return new Order(customer, items, total);
}
@Override
public int type() {
return 1003;
}
@Override
public byte version() {
return 1;
}
@Override
public Class<Order> javaClass() {
return Order.class;
}
}
Null Handling
The write(SerializationContextWrite, Object) method accept null values - the framework handles null values
automatically. Similarly, you should never return null from read(SerializationContextRead, byte).
For nullable fields within your object, use SerializationContextWrite.writeObject(Object) which handles null
correctly:
@Override
public void write(SerializationContextWrite ctx, Person person) {
ctx.writeUTF(person.getName());
ctx.writeObject(person.getAddress()); // Can be null - handled automatically
}
@Override
public Person read(SerializationContextRead ctx, byte version) {
String name = ctx.readUTF();
Address address = ctx.readObject(); // May return null
return new Person(name, address);
}
Backward Compatibility Considerations
- Never remove fields - Old data may still reference them
- Never change field types - The wire format is tied to Java types
- Never reorder fields - Read order must match write order
- Always increment version when adding fields - Allows old deserializers to detect new formats
- Provide defaults for new fields when reading old versions
Warning: The framework does NOT validate these changes in the object schema. It is your responsibility to maintain the contract with the API.
Performance Considerations
Serializers should be stateless and lightweight. The same serializer instance may be used concurrently by multiple threads, so:
- Don't store mutable state in instance fields
- Make serializers thread-safe (stateless implementations are inherently thread-safe)
- Avoid creating unnecessary objects during serialization
- Use primitive write methods (writeInt, writeLong, etc.) instead of boxing
And always remember that serialization is in the hot-path of the algorithm. A slow serialization will harm the throughput in the application level.
Registration
Register your serializers when building the JGroupsRaft instance:
JGroupsRaft<MyStateMachine> raft = JGroupsRaft.builder(stateMachine, MyStateMachine.class)
.registerMarshaller(new PersonMarshaller())
.registerMarshaller(new AddressMarshaller())
.registerMarshaller(new OrderMarshaller())
.build();
Important: All cluster members must register the same serializers with the same type IDs. Failure to do so will result in deserialization errors.
Thread Safety
Serializer implementations must be thread-safe. Multiple threads may invoke write(SerializationContextWrite, T) and read(SerializationContextRead, byte) concurrently
on the same serializer instance.
- Since:
- 2.0
- Author:
- José Bolina
- See Also:
-
Field Summary
Fields -
Method Summary
Modifier and TypeMethodDescriptionReturns the Java class this serializer handles.read(SerializationContextRead ctx, byte version) Reads an object from the input context.inttype()Returns the unique type ID of the current class.byteversion()voidwrite(SerializationContextWrite ctx, T target) Writes the object to the output context.
-
Field Details
-
MINIMUM_TYPE_ID
static final int MINIMUM_TYPE_ID- See Also:
-
-
Method Details
-
write
Writes the object to the output context.The serializer should write all necessary data to reconstruct the object. Do NOT write the type ID - that's handled by the registry.
- Parameters:
ctx- The write contexttarget- The object to serialize (never null)
-
read
Reads an object from the input context.The serializer should read the same data written by
write(SerializationContextWrite, T). The type ID has already been read by the registry.- Parameters:
ctx- The read context- Returns:
- The deserialized object (never null)
-
javaClass
-
type
int type()Returns the unique type ID of the current class.- Returns:
- the type ID.
-
version
byte version()
-