Skip to main content

Examples

note

To run the examples, you need to clone ActiveJ from GitHub

git clone https://github.com/activej/activej

And import it as a Maven project. Check out tag v5.1. Before running the examples, build the project. These examples are located at activej/examples/core/serializer

Simple Object Serialization

In order to create classes whose instances can be serialized/deserialized, you should use special annotations:

  • @Serialize annotation with order number on property getter. Parameter `order` provides better compatibility in case classes are changed.
  • @Deserialize annotation with property name (which should be the same name as the one in getter) in constructor.

This is enough to create serializable POJOs, for example:

public static class Person {  public Person(@Deserialize("age") int age,      @Deserialize("name") String name) {    this.age = age;    this.name = name;  }
  @Serialize  public final int age;
  @Serialize  public final String name;
  private String surname;
  @Serialize  public String getSurname() {    return surname;  }
  public void setSurname(String surname) {    this.surname = surname;  }}

Now let's do some serialization. We'll create a Person instance, a byte array that stores the result of the serialization, and a BinarySerializer instance that represents a serializer that encodes and decodes <T> values to byte arrays (<Person> values in this case):

Person john = new Person(34, "Jim");john.setSurname("Smith");byte[] buffer = new byte[200];BinarySerializer<Person> serializer = SerializerBuilder.create()    .build(Person.class);

That's it, now we can serialize and deserialize our Person instance:

serializer.encode(buffer, 0, john);Person johnCopy = serializer.decode(buffer, 0);

Let's make a simple test to check if everything works correctly:

System.out.println(john.age + " " + johnCopy.age);System.out.println(john.name + " " + johnCopy.name);System.out.println(john.getSurname() + " " + johnCopy.getSurname());

After you run the example, you'll receive the following output:

34 34Jim JimSmith Smith

Which means that the serialization and deserialization worked correctly.

You can explore full example sources on GitHub

Generics and Interfaces

ActiveJ Serializer can simply manage more complex objects. For example, let's see how it works with interfaces and generics.

First, create a simple Skill class:

public static class Skill<K, V> {  private final K key;  private final V value;
  public Skill(@Deserialize("key") K key,      @Deserialize("value") V value) {    this.key = key;    this.value = value;  }
  @Serialize  public K getKey() {    return key;  }
  @Serialize  public V getValue() {    return value;  }}

Next, create a Person interface that has a single method returning a list of skills:

public interface Person<K, V> {  @Serialize  List<Skill<K, V>> getSkills();}

Finally create a Developer class that implements Person interface:

public static class Developer implements Person<Integer, String> {  private List<Skill<Integer, String>> list;
  @Serialize  @Override  public List<Skill<Integer, String>> getSkills() {    return list;  }
  public void setSkills(List<Skill<Integer, String>> list) {    this.list = list;  }}

Let's proceed to the serialization. Similarly to the previous example, we'll create an instance of the Developer, a byte array to store the result of the serialization and an instance of BinarySerializer<Developer> serializer:

Developer developer = new Developer();developer.setSkills(Arrays.asList(    new Skill<>(1, "Java"),    new Skill<>(2, "ActiveJ")));
byte[] buffer = new byte[200];BinarySerializer<Developer> serializer = SerializerBuilder.create()    .build(Developer.class);

Now let's serialize and deserialize our Developer instance:

serializer.encode(buffer, 0, developer);Developer developer2 = serializer.decode(buffer, 0);

Check if the serialization works correctly:

for (int i = 0; i < developer.getSkills().size(); i++) {  System.out.println(developer.getSkills().get(i).getKey() + " - " + developer.getSkills().get(i).getValue() +      ", " + developer2.getSkills().get(i).getKey() + " - " + developer2.getSkills().get(i).getValue());}

If you run the example, you'll receive the following output:

1 - Java, 1 - Java2 - ActiveJ, 2 - ActiveJ

Which means that the serialization worked correctly.

You can explore full example sources on GitHub

Path-based serialization

Sometimes you need to serialize a field that represents a generic type. Let's say a Map. All you need to do is put a @Serialize annotation on that field.

@Serializepublic Map<Integer, String>> map;

But what if you want a map to contain nullable values? You can use @SerializeNullable annotation. However, if you simply put this annotation on a field, it would mean that a whole map can be nullable.

We should put the annotation directly on a String! Starting from ActiveJ v5.0 some serializer annotations are applicable to a type use. So whenever you need to mark some type with additional serializer information, just put the annotation on a type:

@Serializepublic Map<Integer, @SerializeNullable String>> map;

In older versions of ActiveJ the only way to specify which type should be nullable, for example, was by using path option from the annotation. To serialize a map with nullable values you would need to do something like this:

@Serialize@SerializeNullable(path = 1)public Map<Integer, String>> map;

Each type parameter has its own index, starting from 0. So, Integer has an index 0 and String has an index 1. In order to specify that String may be null we add path = 1 to the annotation.

A path parameter is actually an array of ints, so you may specify a nullable type in a nested declaration. You also may put multiple path-based annotations on a field (or getter).

Here is an example:

@Serialize@SerializeNullable // refers to Map<String, Map<Integer, Float[]>>@SerializeNullable(path = 1) // refers to Map<Integer, Float[]@SerializeNullable(path = 0) // refers to String@SerializeNullable(path = {1, 0}) // refers to Integer@SerializeNullable(path = {1, 1}) // refers to Float[]@SerializeNullable(path = {1, 1, 0}) //refers to the Float elements of the arraypublic Map<String, Map<Integer, Float[]>> complexMap;
note

Unfortunately, in some cases the older versions of Java may not properly resolve which type parameter the annotation annotates if a type is annotated. In this case you may use a path-based approach if you run your application using the version of Java less than 12.

In this tutorial we will show you how to write serializers using either path approach or type use approach.

First, let's take a look at a type use approach.

We will define a parameterized class Nested:

public static class Nested<T1, T2> {  @Serialize  public final T1 first;  @Serialize  public final T2 second;
  public Nested(@Deserialize("first") T1 first, @Deserialize("second") T2 second) {    this.first = first;    this.second = second;  }
  @Override  public String toString() {    return "Nested{" + first + ", " + second + '}';  }}

Then we define a Storage class to be serialized:

public static class Storage {  @Serialize  public List<@SerializeNullable Nested<Integer, @SerializeNullable String>> listOfNested;}

A class has a single field which as a List of nullable Nested. In addition, a second type parameter of a Nested class (String) is itself nullable.

We create a serializer as follows:

BinarySerializer<Storage> serializer = SerializerBuilder.create(definingClassLoader)    .build(Storage.class);

We then construct a Storage class and add nullable elements to the list. Once we run the example we should see the following output:

[Nested{1, abc}, null, Nested{5, null}][Nested{1, abc}, null, Nested{5, null}]

This shows both an original Storage contents as well as deserialized one.

note

A special care should be taken when annotating arrays.

@Foo String @Bar []

Here, @Foo annotates String while @Bar annotates the whole array String[].

This is in accordance with Java Language Specification

You can explore full example sources on GitHub

Now, let's see a path based approach.

We will use the same Nested class. However we will annotate a field of Storage class using annotations with path:

public static class Storage {  @Serialize  @SerializeNullable(path = 0)  @SerializeNullable(path = {0, 1})  public List<Nested<Integer, String>> listOfNested;}

When creating a serializer an annotation compatibility mode should be enabled:

BinarySerializer<Storage> serializer = SerializerBuilder.create(definingClassLoader)    .withAnnotationCompatibilityMode() // Compatibility mode has to be enabled    .build(Storage.class);

After running the main method we should see the same output as with type use approach.

[Nested{1, abc}, null, Nested{5, null}][Nested{1, abc}, null, Nested{5, null}]

You can explore full example sources on GitHub

note

You may not mix both annotaion styles. If you want to use type use approach, then use it as is. Just make sure that your version of Java may resolve annotations on types.

For a path based approach do not forget to enable annotation compatibility mode on a SerializerBuilder.

Fixed Size and Nullable Fields Serialization

ActiveJ Serializer has some helper annotations, for example:

  • @SerializeNullable on properties that can have null values. This annotation also has a special `path` parameter. It represent a path of the tree of the variable's data types. It allows to indicate which of the 'nodes' is nullable.

As you can see, you can write several annotations for the different paths of the same data structure.

Let's create a simple example that illustrates how to use these annotations:

public static class Storage {  @Serialize  public @SerializeNullable String @SerializeFixedSize(3) [] strings;
  @Serialize  public byte @SerializeFixedSize(4) [] bytes;}

Now let's serialize and deserialize an instance of the Storage similarly to the previous examples. We'll create an instance of the Storage, a byte array to store the result of the serialization and an instance of BinarySerializer<Storage> serializer:

Storage storage = new Storage();storage.strings = new String[]{"abc", null, "123", "superfluous"};storage.bytes = new byte[]{1, 2, 3, 4, 5, 6};
byte[] buffer = new byte[200];BinarySerializer<Storage> serializer = SerializerBuilder.create()    .build(Storage.class);

Finally, serialize and deserialize Storage instance:

serializer.encode(buffer, 0, storage);Storage limitedStorage = serializer.decode(buffer, 0);

Let's see how serialization affected the storage:

System.out.println(Arrays.toString(storage.strings) + " -> " + Arrays.toString(limitedStorage.strings));System.out.println(Arrays.toString(storage.bytes) + " -> " + Arrays.toString(limitedStorage.bytes));

If you run the example, you'll see the following output:

[abc, null, 123, superfluous] -> [abc, null, 123][1, 2, 3, 4, 5, 6] -> [1, 2, 3, 4]

As you can see in the first line, storage differs from limitedStorage. This is because @SerializeFixedSize annotation was set at value 3 for the strings property. Thus, "superfluous" was removed from the array while serialization took place.

You can explore full example sources on GitHub

Custom serializer

In this example, we will demonstrate how you can write a custom serializer for a LocalDate class. You can use this example as a reference for writing serializers for other classes that you may need to serialize.

Let's imagine we need to serialize a class that contains LocalDate field:

public static class LocalDateHolder {  @Serialize  public final LocalDate date;
  public LocalDateHolder(@Deserialize("date") LocalDate date) {    this.date = date;  }
  @Override  public String toString() {    return "LocalDateHolder{date=" + date + '}';  }}

By default, ActiveJ Serializer does not know how to serialize a LocalDate class, so it would throw an exception if you naively try to serialize it. We have to provide a custom serializer for a LocalDate class to serialize LocalDateHolder class:

public static class SerializerDefLocalDate extends AbstractSerializerDef {
  @Override  public Class<?> getEncodeType() {    return LocalDate.class;  }
  @Override  public Expression encoder(final StaticEncoders staticEncoders,      final Expression buf,      final Variable pos,      final Expression localDate,      final int version,      final CompatibilityLevel compatibilityLevel) {    return sequence(        writeVarInt(buf, pos, call(localDate, "getYear")),        writeVarInt(buf, pos, call(localDate, "getMonthValue")),        writeVarInt(buf, pos, call(localDate, "getDayOfMonth"))    );  }
  @Override  public Expression decoder(final StaticDecoders staticDecoders,      final Expression input,      final int version,      final CompatibilityLevel compatibilityLevel) {    return staticCall(LocalDate.class, "of",        readVarInt(input),        readVarInt(input),        readVarInt(input)    );  }}

We extend AbstractSerializerDef class and implement methods:

  • Class<?> getEncodeType() - specify LocalDate.class as the type of data to be serialized
  • Expression encoder(...) - here we instruct the serializer how to serialize a LocalDate instance. We actually need to serialize 3 int values (year, month, and dayOfMonth) and write them to BinaryOutput Using Lisp-like Expression API we do just that.
  • Expression decoder(...) - here we need to instruct the serializer how to deserialize raw bytes into a LocalDate instance. The process is an inverse to encoding. First, we have to read 3 int values (year, month, and dayOfMonth) from BinaryInput Then we can create a new LocalDate instance by calling static factory method static LocalDate of(int year, int month, int dayOfMonth) and passing previously deserialized int values. We once again use Expression API for this task.

At last, we need to add our serializer of LocalDate to SerializerBuilder

BinarySerializer<LocalDateHolder> serializer =    SerializerBuilder.create()        .with(LocalDate.class, ctx -> new SerializerDefLocalDate())        .build(LocalDateHolder.class);

If we run LocalDateSerializerExample#main method, we should see the following output:

Serializing LocalDateHolder: LocalDateHolder{date=2021-03-17}Byte array with serialized LocalDateHolder: [-27, 15, 3, 17]Deserialized LocalDateHolder: LocalDateHolder{date=2021-03-17}

You can explore full example sources on GitHub