This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Integration 6.4.1! |
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 invokesnew String(payload)
. -
If the payload is a
byte[]
, it invokesnew String(payload, charset)
, wherecharset
is UTF-8 by default. Thecharset
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 |
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 If you wish to set the |
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 defaultSimpleMessageConverter
. -
…→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 defaultJsonMessageConverter
. -
…→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);
}
See also Advising Endpoints Using Annotations.
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.