This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Integration 6.5.1!

Codec

Version 4.2 of Spring Integration introduced the Codec abstraction. Codecs encode and decode objects to and from byte[]. They offer an alternative to Java serialization. One advantage is that, typically, objects need not implement Serializable. We provide one implementation that uses Kryo for serialization, but you can provide your own implementation for use in any of the following components:

  • EncodingPayloadTransformer

  • DecodingTransformer

  • CodecMessageConverter

EncodingPayloadTransformer

This transformer encodes the payload to a byte[] by using the codec. It does not affect message headers.

See the Javadoc for more information.

DecodingTransformer

This transformer decodes a byte[] by using the codec. It needs to be configured with the Class to which the object should be decoded (or an expression that resolves to a Class). If the resulting object is a Message<?>, inbound headers are not retained.

See the Javadoc for more information.

CodecMessageConverter

Certain endpoints (such as TCP and Redis) have no concept of message headers. They support the use of a MessageConverter, and the CodecMessageConverter can be used to convert a message to or from a byte[] for transmission.

See the Javadoc for more information.

Kryo

Currently, this is the only implementation of Codec, and it provides three kinds of Codec:

  • PojoCodec: Used in the transformers

  • MessageCodec: Used in the CodecMessageConverter

  • CompositeCodec: Used in transformers

The framework provides several custom serializers:

  • FileSerializer

  • MessageHeadersSerializer

  • MutableMessageHeadersSerializer

The first can be used with the PojoCodec by initializing it with the FileKryoRegistrar. The second and third are used with the MessageCodec, which is initialized with the MessageKryoRegistrar.

CompositeCodec

The CompositeCodec is a codec that combines multiple codecs into a single codec, delegating encoding and decoding operations to the appropriate type-specific codec. This implementation associates object types with their appropriate codecs while providing a fallback default codec for unregistered types.

An example implementation can be seen below:

void encodeDecodeSample() {
        Codec codec = getFullyQualifiedCodec();

        //Encode and Decode a Dog Object
        Dog dog = new Dog("Wolfy", 3, "woofwoof");
        dog = codec.decode(
                codec.encode(dog),
                Dog.class);
        System.out.println(dog);

        //Encode and Decode a Cat Object
        Cat cat = new Cat("Kitty", 2, 8);
        cat = codec.decode(
                codec.encode(cat),
                Cat.class);
        System.out.println(cat);

        //Use the default code if the type being decoded and encoded is not Cat or dog.
        Animal animal = new Animal("Badger", 5);
        Animal animalOut = codec.decode(
                codec.encode(animal),
                Animal.class);
        System.out.println(animalOut);
}

/**
 * Create and return a {@link CompositeCodec} that associates {@code Dog} and {@code Cat}
 * classes with their respective {@link PojoCodec} instances, while providing a default
 * codec for {@code Animal} types.
 * <p>
 * @return a fully qualified {@link CompositeCodec} for {@code Dog}, {@code Cat},
 *     and fallback for {@code Animal}
 */
static Codec getFullyQualifiedCodec() {
    Map<Class<?>, Codec> codecs = new HashMap<Class<?>, Codec>();
    codecs.put(Dog.class, new PojoCodec(new KryoClassListRegistrar(Dog.class)));
    codecs.put(Cat.class, new PojoCodec(new KryoClassListRegistrar(Cat.class)));
    return new CompositeCodec(codecs, new PojoCodec(
            new KryoClassListRegistrar(Animal.class)));
}

// Records that will be encoded and decoded in this sample
record Dog(String name, int age, String tag) {}
record Cat(String name, int age, int lives) {}
record Animal(String name, int age){}

In some cases a single type of object may return multiple codecs. In these cases an IllegalStateException is thrown.

This class uses ClassUtils.findClosestMatch to select the appropriate codec for a given object type. When multiple codecs match an object type, ClassUtils.findClosestMatch offers the failOnTie option. If failOnTie is false, it will return any one of the matching codecs. If failOnTie is true and multiple codecs match, it will throw an IllegalStateException. CompositeCodec` sets failOnTie to true, so if multiple codecs match, an IllegalStateException is thrown.

Customizing Kryo

By default, Kryo delegates unknown Java types to its FieldSerializer. Kryo also registers default serializers for each primitive type, along with String, Collection, and Map. FieldSerializer uses reflection to navigate the object graph. A more efficient approach is to implement a custom serializer that is aware of the object’s structure and can directly serialize selected primitive fields. The following example shows such a serializer:

public class AddressSerializer extends Serializer<Address> {

    @Override
    public void write(Kryo kryo, Output output, Address address) {
        output.writeString(address.getStreet());
        output.writeString(address.getCity());
        output.writeString(address.getCountry());
    }

    @Override
    public Address read(Kryo kryo, Input input, Class<Address> type) {
        return new Address(input.readString(), input.readString(), input.readString());
    }
}

The Serializer interface exposes Kryo, Input, and Output, which provide complete control over which fields are included and other internal settings, as described in the Kryo documentation.

When registering your custom serializer, you need a registration ID. The registration IDs are arbitrary. However, in our case, the IDs must be explicitly defined, because each Kryo instance across the distributed application must use the same IDs. Kryo recommends small positive integers and reserves a few ids (value < 10). Spring Integration currently defaults to using 40, 41, and 42 (for the file and message header serializers mentioned earlier). We recommend you start at 60, to allow for expansion in the framework. You can override these framework defaults by configuring the registrars mentioned earlier.

Using a Custom Kryo Serializer

If you need custom serialization, see the Kryo documentation, because you need to use the native API to do the customization. For an example, see the org.springframework.integration.codec.kryo.MessageCodec implementation.

Implementing KryoSerializable

If you have write access to the domain object source code, you can implement KryoSerializable as described here. In this case, the class provides the serialization methods itself and no further configuration is required. However, benchmarks have shown this is not quite as efficient as registering a custom serializer explicitly. The following example shows a custom Kryo serializer:

public class Address implements KryoSerializable {

    @Override
    public void write(Kryo kryo, Output output) {
        output.writeString(this.street);
        output.writeString(this.city);
        output.writeString(this.country);
    }

    @Override
    public void read(Kryo kryo, Input input) {
        this.street = input.readString();
        this.city = input.readString();
        this.country = input.readString();
    }
}

You can also use this technique to wrap a serialization library other than Kryo.

Using the @DefaultSerializer Annotation

Kryo also provides a @DefaultSerializer annotation, as described here.

@DefaultSerializer(SomeClassSerializer.class)
public class SomeClass {
       // ...
}

If you have write access to the domain object, this may be a simpler way to specify a custom serializer. Note that this does not register the class with an ID, which may make the technique unhelpful for certain situations.