Transformer

Message transformers play a very important role in enabling the loose-coupling of message producers and message consumers. Rather than requiring every message-producing component to know what type is expected by the next consumer, you can add transformers between those components. Generic transformers, such as one that converts a String to an XML Document, are also highly reusable.

For some systems, it may be best to provide a canonical data model, but Spring Integration’s general philosophy is not to require any particular format. Rather, for maximum flexibility, Spring Integration aims to provide the simplest possible model for extension. As with the other endpoint types, the use of declarative configuration in XML or Java annotations enables simple POJOs to be adapted for the role of message transformers. The rest of this chapter describes these configuration options.

For the sake of maximizing flexibility, Spring does not require XML-based message payloads. Nevertheless, the framework does provide some convenient transformers for dealing with XML-based payloads if that is indeed the right choice for your application. For more information on those transformers, see XML Support - Dealing with XML Payloads.

Configuring a Transformer with Java and other DSLs

For simple Java & Annotation configuration, the Spring bean POJO method must be marked with a @Transformer annotation and the framework calls it when messages are consumed from an input channel:

public class SomeService {

    @Transformer(inputChannel = "transformChannel", outputChannel = "nextServiceChannel")
    public OutputData exampleTransformer(InputData payload) {
        ...
    }

}

See more information in the Annotation Support.

For Java, Groovy or Kotlin DSLs, the .transform() operator of an IntegrationFlow represents a transformer endpoint:

  • Java DSL

  • Kotlin DSL

  • Groovy DSL

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("transformChannel")
             .transform(someService, "exampleTransformer")
             .channel("nextServiceChannel")
             .get();
}
@Bean
fun someFlow() =
    integrationFlow("transformChannel") {
        transform(someService, "exampleTransformer")
        channel("nextServiceChannel")
    }
@Bean
someFlow() {
    integrationFlow 'transformChannel',
            {
                transform someService, 'exampleTransformer'
                channel 'nextServiceChannel'
            }
}

See more information about the DSLs in the respective chapters:

Configuring a Transformer with XML

The <transformer> element is used to create a message-transforming endpoint. In addition to input-channel and output-channel attributes, it requires a ref attribute. The ref may either point to an object that contains the @Transformer annotation on a single method (see Configuring a Transformer with Annotations), or it may be combined with an explicit method name value provided in the method attribute.

<int:transformer id="testTransformer" ref="testTransformerBean" input-channel="inChannel"
             method="transform" output-channel="outChannel"/>
<beans:bean id="testTransformerBean" class="org.foo.TestTransformer" />

Using a ref attribute is generally recommended if the custom transformer handler implementation can be reused in other <transformer> definitions. However, if the custom transformer handler implementation should be scoped to a single definition of the <transformer>, you can define an inner bean definition, as the following example shows:

<int:transformer id="testTransformer" input-channel="inChannel" method="transform"
                output-channel="outChannel">
  <beans:bean class="org.foo.TestTransformer"/>
</transformer>
Using both the ref attribute and an inner handler definition in the same <transformer> configuration is not allowed, as it creates an ambiguous condition and results in an exception being thrown.
If the ref attribute references a bean that extends AbstractMessageProducingHandler (such as transformers provided by the framework itself), the configuration is optimized by injecting the output channel into the handler directly. In this case, each ref must be to a separate bean instance (or a prototype-scoped bean) or use the inner <bean/> configuration type. If you inadvertently reference the same message handler from multiple beans, you get a configuration exception.

When using a POJO, the method that is used for transformation may expect either the Message type or the payload type of inbound messages. It may also accept message header values either individually or as a full map by using the @Header and @Headers parameter annotations, respectively. The return value of the method can be any type. If the return value is itself a Message, that is passed along to the transformer’s output channel.

As of Spring Integration 2.0, a message transformer’s transformation method can no longer return null. Returning null results in an exception, because a message transformer should always be expected to transform each source message into a valid target message. In other words, a message transformer should not be used as a message filter, because there is a dedicated <filter> option for that. However, if you do need this type of behavior (where a component might return null and that should not be considered an error), you could use a service activator. Its requires-reply value is false by default, but that can be set to true in order to have exceptions thrown for null return values, as with the transformer.

Transformers and Spring Expression Language (SpEL)

Like routers, aggregators, and other components, as of Spring Integration 2.0, transformers can also benefit from SpEL support whenever transformation logic is relatively simple. The following example shows how to use a SpEL expression:

<int:transformer input-channel="inChannel"
	output-channel="outChannel"
	expression="payload.toUpperCase() + '- [' + T(System).currentTimeMillis() + ']'"/>

The preceding example transforms the payload without writing a custom transformer. Our payload (assumed to be a String) is upper-cased, concatenated with the current timestamp, and has some formatting applied.

Common Transformers

Spring Integration provides a few transformer implementations.

Object-to-String Transformer

Because it is fairly common to use the toString() representation of an Object, Spring Integration provides an ObjectToStringTransformer (see also the Transformers factory) where the output is a Message with a String payload. That String is the result of invoking the toString() operation on the inbound Message’s payload. The following example shows how to declare an instance of the object-to-string transformer:

  • Java DSL

  • Kotlin DSL

  • Groovy DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("in")
             .transform(Transformers.objectToString())
             .channel("out")
             .get();
}
@Bean
fun someFlow() =
    integrationFlow("in") {
        transform(Transformers.objectToString())
        channel("out")
    }
@Bean
someFlow() {
    integrationFlow 'in',
            {
                transform Transformers.objectToString()
                channel 'out'
            }
}
<int:object-to-string-transformer input-channel="in" output-channel="out"/>

A potential use for this transformer would be sending some arbitrary object to the 'outbound-channel-adapter' in the file namespace. Whereas that channel adapter only supports String, byte-array, or java.io.File payloads by default, adding this transformer immediately before the adapter handles the necessary conversion. That works fine as long as the result of the toString() call is what you want to be written to the file. Otherwise, you can provide a custom POJO-based transformer by using the generic 'transformer' element shown previously.

When debugging, this transformer is not typically necessary, since the logging-channel-adapter is capable of logging the message payload. See Wire Tap for more detail.

The object-to-string transformer is very simple. It invokes toString() on the inbound payload. Since Spring Integration 3.0, there are two exceptions to this rule:

  • If the payload is a char[], it invokes new String(payload).

  • If the payload is a byte[], it invokes new String(payload, charset), where charset is UTF-8 by default. The charset can be modified by supplying the charset attribute on the transformer.

For more sophistication (such as selection of the charset dynamically, at runtime), you can use a SpEL expression-based transformer instead, as the following example shows:

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("in")
             .transform("new String(payload, headers['myCharset']")
             .channel("out")
             .get();
}
<int:transformer input-channel="in" output-channel="out"
       expression="new String(payload, headers['myCharset']" />

If you need to serialize an Object to a byte array or deserialize a byte array back into an Object, Spring Integration provides symmetrical serialization transformers. These use standard Java serialization by default, but you can provide an implementation of Spring Serializer or Deserializer strategies by using the serializer and deserializer attributes, respectively. See also the Transformers factory class. The following example shows to use Spring’s serializer and deserializer:

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("objectsIn")
             .transform(Transformers.serializer())
             .channel("bytesOut")
             .channel("bytesIn")
             .transform(Transformers.deserializer("com.mycom.*", "com.yourcom.*"))
             .channel("objectsOut")
             .get();
}
<int:payload-serializing-transformer input-channel="objectsIn" output-channel="bytesOut"/>

<int:payload-deserializing-transformer input-channel="bytesIn" output-channel="objectsOut"
    allow-list="com.mycom.*,com.yourcom.*"/>
When deserializing data from untrusted sources, you should consider adding a allow-list of package and class patterns. By default, all classes are deserialized.

Object-to-Map and Map-to-Object Transformers

Spring Integration also provides Object-to-Map and Map-to-Object transformers, which use the JSON to serialize and de-serialize the object graphs. The object hierarchy is introspected to the most primitive types (String, int, and so on). The path to this type is described with SpEL, which becomes the key in the transformed Map. The primitive type becomes the value.

Consider the following example:

public class Parent{
    private Child child;
    private String name;
    // setters and getters are omitted
}

public class Child{
    private String name;
    private List<String> nickNames;
    // setters and getters are omitted
}

The two classes in the preceding example are transformed to the following Map:

{person.name=George, person.child.name=Jenna, person.child.nickNames[0]=Jen ...}

The JSON-based Map lets you describe the object structure without sharing the actual types, which lets you restore and rebuild the object graph into a differently typed object graph, as long as you maintain the structure.

For example, the preceding structure could be restored back to the following object graph by using the Map-to-Object transformer:

public class Father {
    private Kid child;
    private String name;
    // setters and getters are omitted
}

public class Kid {
    private String name;
    private List<String> nickNames;
    // setters and getters are omitted
}

If you need to create a “structured” map, you can provide the flatten attribute. The default is 'true'. If you set it to 'false', the structure is a Map of Map objects.

Consider the following example:

public class Parent {
	private Child child;
	private String name;
	// setters and getters are omitted
}

public class Child {
	private String name;
	private List<String> nickNames;
	// setters and getters are omitted
}

The two classes in the preceding example are transformed to the following Map:

{name=George, child={name=Jenna, nickNames=[Bimbo, ...]}}

To configure these transformers, Spring Integration provides respective XML component and Java DSL factory:

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("directInput")
             .transform(Transformers.toMap())
             .channel("output")
             .get();
}
<int:object-to-map-transformer input-channel="directInput" output-channel="output"/>

You can also set the flatten attribute to false, as follows:

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("directInput")
             .transform(Transformers.toMap(false))
             .channel("output")
             .get();
}
<int:object-to-map-transformer input-channel="directInput" output-channel="output" flatten="false"/>

Spring Integration provides XML namespace support for Map-to-Object and the Java DSL factory has the fromMap() method, as the following example shows:

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("input")
             .transform(Transformers.fromMap(org.something.Person.class))
             .channel("output")
             .get();
}
<int:map-to-object-transformer input-channel="input"
                         output-channel="output"
                         type="org.something.Person"/>

Alternatively, you could use a ref attribute and a prototype-scoped bean, as the following example shows:

  • Java DSL

  • XML

@Bean
IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("inputA")
             .transform(Transformers.fromMap("person"))
             .channel("outputA")
             .get();
}

@Bean
@Scope("prototype")
Person person() {
    return new Person();
}
<int:map-to-object-transformer input-channel="inputA"
                               output-channel="outputA"
                               ref="person"/>
<bean id="person" class="org.something.Person" scope="prototype"/>
The 'ref' and 'type' attributes are mutually exclusive. Also, if you use the 'ref' attribute, you must point to a 'prototype' scoped bean. Otherwise, a BeanCreationException is thrown.

Starting with version 5.0, you can supply the ObjectToMapTransformer with a customized JsonObjectMapper — for when you need special formats for dates or nulls for empty collections (and other uses). See JSON Transformers for more information about JsonObjectMapper implementations.

Stream Transformer

The StreamTransformer transforms InputStream payloads to a byte[]( or a String if a charset is provided).

The following example shows how to use the stream-transformer element in XML:

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("input")
             .transform(Transformers.fromStream("UTF-8"))
             .channel("output")
             .get();
}
<int:stream-transformer input-channel="directInput" output-channel="output"/> <!-- byte[] -->

<int:stream-transformer id="withCharset" charset="UTF-8"
    input-channel="charsetChannel" output-channel="output"/> <!-- String -->

The following example shows how to use the StreamTransformer class and the @Transformer annotation to configure a stream transformer in Java:

@Bean
@Transformer(inputChannel = "stream", outputChannel = "data")
public StreamTransformer streamToBytes() {
    return new StreamTransformer(); // transforms to byte[]
}

@Bean
@Transformer(inputChannel = "stream", outputChannel = "data")
public StreamTransformer streamToString() {
    return new StreamTransformer("UTF-8"); // transforms to String
}

JSON Transformers

Spring Integration provides Object-to-JSON and JSON-to-Object transformers. The following pair of examples show how to declare them in XML:

<int:object-to-json-transformer input-channel="objectMapperInput"/>

<int:json-to-object-transformer input-channel="objectMapperInput"
    type="foo.MyDomainObject"/>

By default, the transformers in the preceding listing use a vanilla JsonObjectMapper. It is based on an implementation from the classpath. You can provide your own custom JsonObjectMapper implementation with appropriate options or based on a required library (such as GSON), as the following example shows:

<int:json-to-object-transformer input-channel="objectMapperInput"
    type="something.MyDomainObject" object-mapper="customObjectMapper"/>

Beginning with version 3.0, the object-mapper attribute references an instance of a new strategy interface: JsonObjectMapper. This abstraction lets multiple implementations of JSON mappers be used. Implementation that wraps Jackson 2 is provided, with the version being detected on the classpath. The class is Jackson2JsonObjectMapper, respectively.

You may wish to consider using a FactoryBean or a factory method to create the JsonObjectMapper with the required characteristics. The following example shows how to use such a factory:

public class ObjectMapperFactory {

    public static Jackson2JsonObjectMapper getMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
        return new Jackson2JsonObjectMapper(mapper);
    }
}

The following example shows how to do the same thing in XML:

<bean id="customObjectMapper" class="something.ObjectMapperFactory"
            factory-method="getMapper"/>

Beginning with version 2.2, the object-to-json-transformer sets the content-type header to application/json, by default, if the input message does not already have that header.

If you wish to set the content-type header to some other value or explicitly overwrite any existing header with some value (including application/json), use the content-type attribute. If you wish to suppress the setting of the header, set the content-type attribute to an empty string (""). Doing so results in a message with no content-type header, unless such a header was present on the input message.

Beginning with version 3.0, the ObjectToJsonTransformer adds headers, reflecting the source type, to the message. Similarly, the JsonToObjectTransformer can use those type headers when converting the JSON to an object. These headers are mapped in the AMQP adapters so that they are entirely compatible with the Spring-AMQP JsonMessageConverter.

This enables the following flows to work without any special configuration:

  • …​→amqp-outbound-adapter---→

  • ---→amqp-inbound-adapter→json-to-object-transformer→…​

    Where the outbound adapter is configured with a JsonMessageConverter and the inbound adapter uses the default SimpleMessageConverter.

  • …​→object-to-json-transformer→amqp-outbound-adapter---→

  • ---→amqp-inbound-adapter→…​

    Where the outbound adapter is configured with a SimpleMessageConverter and the inbound adapter uses the default JsonMessageConverter.

  • …​→object-to-json-transformer→amqp-outbound-adapter---→

  • ---→amqp-inbound-adapter→json-to-object-transformer→

    Where both adapters are configured with a SimpleMessageConverter.

When using the headers to determine the type, you should not provide a class attribute, because it takes precedence over the headers.

In addition to JSON Transformers, Spring Integration provides a built-in #jsonPath SpEL function for use in expressions. For more information see Spring Expression Language (SpEL).

Since version 3.0, Spring Integration also provides a built-in #xpath SpEL function for use in expressions. For more information see #xpath SpEL Function.

Beginning with version 4.0, the ObjectToJsonTransformer supports the resultType property, to specify the node JSON representation. The result node tree representation depends on the implementation of the provided JsonObjectMapper. By default, the ObjectToJsonTransformer uses a Jackson2JsonObjectMapper and delegates the conversion of the object to the node tree to the ObjectMapper#valueToTree method. The node JSON representation provides efficiency for using the JsonPropertyAccessor when the downstream message flow uses SpEL expressions with access to the properties of the JSON data. See Property Accessors for more information.

Beginning with version 5.1, the resultType can be configured as BYTES to produce a message with the byte[] payload for convenience when working with downstream handlers which operate with this data type.

Starting with version 5.2, the JsonToObjectTransformer can be configured with a ResolvableType to support generics during deserialization with the target JSON processor. Also, this component now consults request message headers first for the presence of the JsonHeaders.RESOLVABLE_TYPE or JsonHeaders.TYPE_ID and falls back to the configured type otherwise. The ObjectToJsonTransformer now also populates a JsonHeaders.RESOLVABLE_TYPE header based on the request message payload for any possible downstream scenarios.

Starting with version 5.2.6, the JsonToObjectTransformer can be supplied with a valueTypeExpression to resolve a ResolvableType for the payload to convert from JSON at runtime against the request message. By default, it consults JsonHeaders in the request message. If this expression returns null or ResolvableType building throws a ClassNotFoundException, the transformer falls back to the provided targetType. This logic is present as an expression because JsonHeaders may not have real class values, but rather some type ids which have to be mapped to target classes according some external registry.

Apache Avro Transformers

Version 5.2 added simple transformers to transform to/from Apache Avro.

They are unsophisticated in that there is no schema registry; the transformers simply use the schema embedded in the SpecificRecord implementation generated from the Avro schema.

Messages sent to the SimpleToAvroTransformer must have a payload that implements SpecificRecord; the transformer can handle multiple types. The SimpleFromAvroTransformer must be configured with a SpecificRecord class which is used as the default type to deserialize. You can also specify a SpEL expression to determine the type to deserialize using the setTypeExpression method. The default SpEL expression is headers[avro_type] (AvroHeaders.TYPE) which, by default, is populated by the SimpleToAvroTransformer with the fully qualified class name of the source class. If the expression returns null, the defaultType is used.

The SimpleToAvroTransformer also has a setTypeExpression method. This allows decoupling of the producer and consumer where the sender can set the header to some token representing the type and the consumer then maps that token to a type.

Protocol Buffers Transformers

Version 6.1 adds support for transforming from and to Protocol Buffers data content.

The ToProtobufTransformer transforms a com.google.protobuf.Message message payloads into native byte array or json text payloads. The application/x-protobuf content type (used by default) produces byte array output payload. If the content type is application/json add the com.google.protobuf:protobuf-java-util if found on the classpath, then the output is text json payload. If the content type header is not set the ToProtobufTransformer defaults to application/x-protobuf.

The FromProtobufTransformer transforms byte array or text protobuf payload (depending on the content type) back into com.google.protobuf.Message instances. The FromProtobufTransformer should specify either an expected class type explicitly (use the setExpectedType method) or use a SpEL expression to determine the type to deserialize using the setExpectedTypeExpression method. The default SpEL expression is headers[proto_type] (ProtoHeaders.TYPE) which is populated by the ToProtobufTransformer with the fully qualified class name of the source com.google.protobuf.Message class.

For example, compiling the following IDL:

syntax = "proto2";
package tutorial;

option java_multiple_files = true;
option java_package = "org.example";
option java_outer_classname = "MyProtos";

message MyMessageClass {
  optional string foo = 1;
  optional string bar = 2;
}

will generate a new org.example.MyMessageClass class.

Then use the:

// Transforms a MyMessageClass instance into a byte array.
ToProtobufTransformer toTransformer = new ToProtobufTransformer();

MyMessageClass test = MyMessageClass.newBuilder()
                                .setFoo("foo")
                                .setBar("bar")
                                .build();
// message1 payload is byte array protocol buffer wire format.
Message message1 = toTransformer.transform(new GenericMessage<>(test));

// Transforms a byte array payload into a MyMessageClass instance.
FromProtobufTransformer fromTransformer = new FromProtobufTransformer();

// message2 payload == test
Message message2 =  fromTransformer.transform(message1);

Configuring a Transformer with Annotations

You can add the @Transformer annotation to methods that expect either the Message type or the message payload type. The return value is handled in the exact same way as described earlier in the section describing the <transformer> element. The following example shows how to use the @Transformer annotation to transform a String into an Order:

@Transformer
Order generateOrder(String productId) {
    return new Order(productId);
}

Transformer methods can also accept the @Header and @Headers annotations, as documented in Annotation Support. The following examples shows how to use the @Header annotation:

@Transformer
Order generateOrder(String productId, @Header("customerName") String customer) {
    return new Order(productId, customer);
}

Header Filter

Sometimes, your transformation use case might be as simple as removing a few headers. For such a use case, Spring Integration provides a header filter that lets you specify certain header names that should be removed from the output message (for example, removing headers for security reasons or a value that was needed only temporarily). Basically, the header filter is the opposite of the header enricher. The latter is discussed in Header Enricher. The following example defines a header filter:

  • Java DSL

  • XML

@Bean
public IntegrationFlow someFlow() {
    return IntegrationFlow
             .from("inputChannel")
             .headerFilter("lastName", "state")
             .channel("outputChannel")
             .get();
}
<int:header-filter input-channel="inputChannel"
		output-channel="outputChannel" header-names="lastName, state"/>

As you can see, configuration of a header filter is quite simple. It is a typical endpoint with input and output channels and a header-names attribute. That attribute accepts the names of the headers (delimited by commas if there are multiple) that need to be removed. So, in the preceding example, the headers named 'lastName' and 'state' are not present on the outbound message.

Codec-Based Transformers

See Codec.