Files
spring-integration/src/reference/asciidoc/transformer.adoc
2020-11-19 14:04:25 -05:00

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>>.