Enumerable Ways to Serialize

With Hazelcast there’s a plethora of ways to do serialization. Personally, I prefer to use either ByteArraySerializer or StreamSerializer, for a couple of reasons:

  1. They facilitate separation of concerns, i.e. the serialization code is separate from the class itself.
  2. Because they facilitate separation of concerns, there’s no required dependency on Hazelcast in the actual classes being serialized. This is particularly important if you don’t own or control the classes, and thus cannot implement any of the Hazelcast serialization interfaces.
  3. Full constructor freedom. Because the instantiation is done in the Serializer class, which you write and control, there’s full freedom in how the class is designed, e.g. no need for a default constructor.

Both ByteArraySerializer and StreamSerializer extend Serializer, which has method int getTypeId(). This is used by Hazelcast to determine the class being serialized, rather than using the full class name, saving valuable bytes when serializing. However, it can quickly get a little hairy keeping track of type ids and ensuring that they are unique across the code base. Of course Hazelcast will complain if the same type id is being registered twice, but nobody likes a runtime error. One way to handle this is to leverage an enum as a collection of serializers and use the ordinal() value to determine the type id.

First, we need to define an interface for serialization only, effectively copying the two StreamSerializer methods for reading and writing (or ByteArraySerializer if your prefer):

MySerializer.java:

package foo.serialization;
    
    interface MySerializer {
        Class typeClass();
        T read(ObjectDataInput inp) throws IOException;
        void write(ObjectDataOutput out, T obj) throws IOException;
    }

Next, let’s define an immutable class to serialize, the ubiquitous “Person”.

Person.java:

package foo.domain;
    
    import java.time.LocalDate;
    
    public class Person {
        private final String name;
        private final LocalDate bday;
        public Person(String name, LocalDate bday) {
            this.name = name;
            this.bday = bday;
        }
        public String getName() {
            return this.name;
        }
        public LocalDate getBirthday() {
            return this.bday;
        }
    }

With that as our target, let’s write the serializer.

PersonSerializer.java:

package foo.serialization;
    
    import foo.domain.Person;
    
    class PersonSerializer implements MySerializer {
        public Class typeClass() {
            return Person.class;
        }
        public void write(ObjectDataOutput out, Person person) throws IOException {
            out.writeUTF(person.getName());
            out.writeShort(person.getBirthday().getYear());
            out.write(person.getBirthday().getMonthValue());
            out.write(person.getBirthday().getDayOfMonth());
        }
        public Person read(ObjectDataInput inp) throws IOException {
            String name = inp.readUTF();
            int year = inp.readShort();
            int month = inp.readByte();
            int day = inp.readByte();
            return new Person(name, LocalDate.of(year, month, day));
        }
    }

And now we can finally define the enum:

MySerializers.java:

package foo.serialization;
    
    import com.hazelcast.nio.serialization.StreamSerializer;
    
    public enum MySerializers implements StreamSerializer {
        Person(new PersonSerializer());
    
        private final MySerializer serializer;
    
        private MySerializers(MySerializer serializer) {
            this.serializer = serializer;
        }
    
        public int getTypeId() {
            // Leverage ordinal to keep track of ids.
            // Hazelcast requires > 0, so we add 1
            return ordinal() + 1;
        }
        public void destroy() {} // Nothing to clean up.
        public Object read(ObjectDataInput inp) throws IOException {
            return this.serializer.read(inp); // Forward to implementation
        }
        public void write(ObjectDataOutput out, Object obj) throws IOException {
            this.serializer.write(out, obj); // Forward to implementation
        }
    
        /** Register all serializers. */
        public static void register(SerializationConfig config) {
            for (MySerializers ser : MySerializers.values()) {
                SerializerConfig sc = new SerializerConfig();
                sc.setImplementation(ser).setTypeClass(ser.serializer.typeClass());
                config.addSerializerConfig(sc);
            }
        }
    }

Now all that’s left to do is registrering the serializers at startup:

Member.java:

import com.hazelcast.config.Config;
    import com.hazelcast.core.Hazelcast;
    import com.hazelcast.core.HazelcastInstance;
    
    public class Member {
        public static void main(String[] args) {
            Config config = new Config();
            MySerializers.register(config.getSerializationConfig());
            HazelcastInstance hz = Hazelcast.newHazelcastInstance(config);
        }
    }

And similarly for clients:

Client.java:

import com.hazelcast.client.HazelcastClient;
    import com.hazelcast.client.config.ClientConfig;
    import com.hazelcast.core.HazelcastInstance;
    
    public class Client {
        public static void main(String[] args) {
            ClientConfig config = new ClientConfig();
            MySerializers.register(config.getSerializationConfig());
            HazelcastInstance hz = HazelcastClient.newHazelcastClient(config);
        }
    }

With this in place, it’s pretty straightforward how to add more classes. Just implement another MySerializer and add it to the MySerializers enum, the rest is handled.