507 lines
23 KiB
Plaintext
507 lines
23 KiB
Plaintext
[[transformer]]
|
|
=== 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 https://www.enterpriseintegrationpatterns.com/CanonicalDataModel.html[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.
|
|
|
|
NOTE: 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.adoc#xml,XML Support - Dealing with XML Payloads>>.
|
|
|
|
[[transformer-namespace]]
|
|
==== 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 <<transformer-annotation>>), or it may be combined with an explicit method name value provided in the `method` attribute.
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<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:
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<int:transformer id="testTransformer" input-channel="inChannel" method="transform"
|
|
output-channel="outChannel">
|
|
<beans:bean class="org.foo.TestTransformer"/>
|
|
</transformer>
|
|
----
|
|
====
|
|
|
|
NOTE: 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.
|
|
|
|
IMPORTANT: 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 https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions[SpEL support] whenever transformation logic is relatively simple.
|
|
The following example shows how to use a SpEL expression:
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<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` whose 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:
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<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.
|
|
|
|
TIP: When debugging, this transformer is not typically necessary, since the `logging-channel-adapter` is capable of logging the message payload.
|
|
See <<./channel.adoc#channel-wiretap,Wire Tap>> for more detail.
|
|
|
|
[NOTE]
|
|
====
|
|
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:
|
|
|
|
[source,xml]
|
|
----
|
|
<int:transformer input-channel="in" output-channel="out"
|
|
expression="new java.lang.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.
|
|
The following example shows to use Spring's serializer and deserializer:
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<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.*"/>
|
|
----
|
|
====
|
|
|
|
IMPORTANT: 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:
|
|
|
|
====
|
|
[source,java]
|
|
----
|
|
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`:
|
|
|
|
====
|
|
[source]
|
|
----
|
|
{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:
|
|
|
|
====
|
|
[source,java]
|
|
----
|
|
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:
|
|
|
|
====
|
|
[source,java]
|
|
----
|
|
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`:
|
|
|
|
====
|
|
[source]
|
|
----
|
|
{name=George, child={name=Jenna, nickNames=[Bimbo, ...]}}
|
|
----
|
|
====
|
|
|
|
To configure these transformers, Spring Integration provides namespace support for Object-to-Map, as the following example shows:
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<int:object-to-map-transformer input-channel="directInput" output-channel="output"/>
|
|
----
|
|
====
|
|
|
|
You can also set the `flatten` attribute to false, as follows:
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<int:object-to-map-transformer input-channel="directInput" output-channel="output" flatten="false"/>
|
|
----
|
|
====
|
|
|
|
Spring Integration provides namespace support for Map-to-Object, as the following example shows:
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<int:map-to-object-transformer input-channel="input"
|
|
output-channel="output"
|
|
type="org.something.Person"/>
|
|
----
|
|
====
|
|
|
|
Alterately, you could use a `ref` attribute and a prototype-scoped bean, as the following example shows:
|
|
[source,xml]
|
|
----
|
|
<int:map-to-object-transformer input-channel="inputA"
|
|
output-channel="outputA"
|
|
ref="person"/>
|
|
<bean id="person" class="org.something.Person" scope="prototype"/>
|
|
|
|
----
|
|
|
|
NOTE: 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]]
|
|
===== 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:
|
|
|
|
====
|
|
[source, xml]
|
|
----
|
|
<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:
|
|
|
|
====
|
|
[source, 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]]
|
|
===== 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:
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<int:object-to-json-transformer input-channel="objectMapperInput"/>
|
|
----
|
|
|
|
[source,xml]
|
|
----
|
|
<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:
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<int:json-to-object-transformer input-channel="objectMapperInput"
|
|
type="something.MyDomainObject" object-mapper="customObjectMapper"/>
|
|
----
|
|
====
|
|
|
|
[NOTE]
|
|
====
|
|
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 https://github.com/FasterXML[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:
|
|
|
|
====
|
|
[source,java]
|
|
----
|
|
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
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<bean id="customObjectMapper" class="something.ObjectMapperFactory"
|
|
factory-method="getMapper"/>
|
|
----
|
|
====
|
|
|
|
[IMPORTANT]
|
|
====
|
|
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.
|
|
|
|
It 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 https://docs.spring.io/spring-amqp/api/[`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`.
|
|
|
|
NOTE: 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 <<./spel.adoc#spel,Spring Expression Language (SpEL)>>.
|
|
|
|
[[transformer-xpath-spel-function]]
|
|
Since version 3.0, Spring Integration also provides a built-in `#xpath` SpEL function for use in expressions.
|
|
For more information see <<./xml.adoc#xpath-spel-function,#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 <<./spel.adoc#spel-property-accessors,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.
|
|
|
|
[[Avro-transformers]]
|
|
===== 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.
|
|
|
|
[[transformer-annotation]]
|
|
==== 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 <<transformer-namespace,in the section describing the `<transformer>` element>>.
|
|
The following example shows how to use the `@Transformer` annotation to transform a `String` into an `Order`:
|
|
|
|
====
|
|
[source,java]
|
|
----
|
|
@Transformer
|
|
Order generateOrder(String productId) {
|
|
return new Order(productId);
|
|
}
|
|
----
|
|
====
|
|
|
|
Transformer methods can also accept the `@Header` and `@Headers` annotations, as documented in `<<./configuration.adoc#annotations,Annotation Support>>`.
|
|
The following examples shows how to use the `@Header` annotation:
|
|
|
|
====
|
|
[source,java]
|
|
----
|
|
@Transformer
|
|
Order generateOrder(String productId, @Header("customerName") String customer) {
|
|
return new Order(productId, customer);
|
|
}
|
|
----
|
|
====
|
|
|
|
See also <<./handler-advice.adoc#advising-with-annotations,Advising Endpoints Using Annotations>>.
|
|
|
|
[[header-filter]]
|
|
==== 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 <<./content-enrichment.adoc#header-enricher,Header Enricher>>.
|
|
The following example defines a header filter:
|
|
|
|
====
|
|
[source,xml]
|
|
----
|
|
<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.adoc#codec,Codec>>.
|