Transformer
消息转换器在实现消息生产者和消息消费者松耦合方面发挥着非常重要的作用。与其要求每个消息生成组件都知道下一个消费者期望的类型,不如在这两个组件之间添加转换器。通用转换器,例如将 String
转换为 XML 文档的转换器,也具有很高的可重用性。
对于某些系统,提供一个规范数据模型可能是最好的选择,但 Spring Integration 的总体理念是不要求任何特定格式。相反,为了最大限度的灵活性,Spring Integration 致力于提供最简单的扩展模型。与其他端点类型一样,使用 XML 或 Java 注解的声明式配置可以将简单的 POJO 适应为消息转换器的角色。本章的其余部分将描述这些配置选项。
为了最大限度地提高灵活性,Spring 不要求基于 XML 的消息有效负载。然而,如果这确实是你的应用程序的正确选择,框架确实为处理基于 XML 的有效负载提供了一些方便的转换器。有关这些转换器的更多信息,请参阅 XML 支持 - 处理 XML 有效负载。
使用 Java 和其他 DSL 配置 Transformer
对于简单的 Java & 注解配置,Spring bean POJO 方法必须标记有 @Transformer
注解,当从输入通道消费消息时,框架会调用该方法:
public class SomeService {
@Transformer(inputChannel = "transformChannel", outputChannel = "nextServiceChannel")
public OutputData exampleTransformer(InputData payload) {
...
}
}
请在 Annotation Support 中查看更多信息。
对于 Java、Groovy 或 Kotlin DSL,IntegrationFlow
的 .transform()
操作符表示一个转换器端点:
- 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'
}
}
有关DSL的更多信息,请参见相关章节:
使用 XML 配置 Transformer
<transformer>
元素用于创建消息转换端点。除了 input-channel
和 output-channel
属性外,它还需要一个 ref
属性。ref
可以指向包含单个方法上的 @Transformer
注解的对象(参见 使用注解配置 Transformer),也可以与 method
属性中提供的显式方法名称值结合使用。
<int:transformer id="testTransformer" ref="testTransformerBean" input-channel="inChannel"
method="transform" output-channel="outChannel"/>
<beans:bean id="testTransformerBean" class="org.foo.TestTransformer" />
使用 ref
属性通常是推荐的做法,如果自定义转换器处理程序的实现可以在其他 <transformer>
定义中重用的话。但是,如果自定义转换器处理程序的实现应该仅限于单个 <transformer>
定义的作用域内,你可以定义一个内部的 bean 定义,如下例所示:
<int:transformer id="testTransformer" input-channel="inChannel" method="transform"
output-channel="outChannel">
<beans:bean class="org.foo.TestTransformer"/>
</transformer>
在同一 <transformer>
配置中使用 ref
属性和内部处理器定义是不允许的,因为它会创建一个模棱两可的条件,并导致抛出异常。
如果 ref
属性引用了一个扩展 AbstractMessageProducingHandler
的 bean(例如框架本身提供的转换器),则通过直接将输出通道注入处理程序来优化配置。在这种情况下,每个 ref
必须引用一个单独的 bean 实例(或 prototype
范围的 bean)或使用内部 <bean/>
配置类型。如果您不小心从多个 bean 引用了相同的消息处理程序,则会得到一个配置异常。
当使用 POJO 时,用于转换的方法可能期望 Message
类型或传入消息的有效载荷类型。它也可以通过分别使用 @Header
和 @Headers
参数注解来接受单个消息头值或完整的头映射。方法的返回值可以是任何类型。如果返回值本身是一个 Message
,那么它将被传递到转换器的输出通道。
从 Spring Integration 2.0 开始,消息转换器的转换方法不能再返回 null
。返回 null
会导致异常,因为消息转换器应该始终将每个源消息转换为有效的目标消息。换句话说,消息转换器不应该用作消息过滤器,因为为此有一个专门的 <filter>
选项。然而,如果你确实需要这种行为(即组件可能会返回 null
,并且这不应该被视为错误),你可以使用服务激活器。它的 requires-reply
值默认为 false
,但可以将其设置为 true
以在返回 null
时抛出异常,就像转换器一样。
Transformers 和 Spring 表达式语言 (SpEL)
像路由器、聚合器和其他组件一样,从 Spring Integration 2.0 开始,转换器也可以在转换逻辑相对简单时受益于 SpEL 支持。以下示例展示了如何使用 SpEL 表达式:
<int:transformer input-channel="inChannel"
output-channel="outChannel"
expression="payload.toUpperCase() + '- [' + T(System).currentTimeMillis() + ']'"/>
前面的例子在不编写自定义转换器的情况下转换了有效负载。我们的有效负载(假设为 String
类型)被转换为大写,与当前时间戳连接在一起,并应用了一些格式。
常见转换器
Spring Integration 提供了几种转换器实现。
对象到字符串转换器
因为使用 Object
的 toString()
表示相当常见,Spring Integration 提供了一个 ObjectToStringTransformer
(也请参阅 Transformers
工厂),其输出是一个带有字符串 payload
的 Message
。该 String
是通过调用入站消息的有效负载的 toString()
操作得到的结果。以下示例展示了如何声明对象到字符串转换器的一个实例:
- 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"/>
此转换器的一个潜在用途是将某个任意对象发送到 file
命名空间中的 'outbound-channel-adapter'。而该通道适配器默认只支持 String
、字节数组或 java.io.File
类型的有效负载,所以在适配器前添加此转换器可以处理必要的转换。只要 toString()
方法的返回结果是你希望写入文件的内容,这种方式就可以正常工作。否则,你可以通过使用前面展示的通用 'transformer' 元素提供一个基于自定义 POJO 的转换器。
在调试时,通常不需要这个转换器,因为 logging-channel-adapter
能够记录消息有效负载。更多详情请参见 线路插入。
对象转字符串转换器非常简单。它对传入的有效载荷调用 toString()
。自 Spring Integration 3.0 起,有两个例外情况:
-
如果有效载荷是
char[]
,它会调用new String(payload)
。 -
如果有效载荷是
byte[]
,它会调用new String(payload, charset)
,其中charset
默认是 UTF-8。可以通过在转换器上提供字符集属性来修改charset
。
对于更复杂的情况(例如在运行时动态选择字符集),你可以使用基于 SpEL 表达式的转换器,如下例所示:
- 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']" />
如果你需要将一个 Object
序列化为字节数组或将字节数组反序列化回 Object
,Spring Integration 提供了对称的序列化转换器。这些默认使用标准 Java 序列化,但是你可以通过使用 serializer
和 deserializer
属性提供 Spring Serializer
或 Deserializer
策略的实现。另请参阅 Transformers
工厂类。以下示例展示了如何使用 Spring 的序列化程序和反序列化程序:
- 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.*"/>
在从不可信来源反序列化数据时,您应该考虑添加一个 allow-list
来指定允许的包和类模式。默认情况下,所有类都会被反序列化。
Object
-to-Map
和 Map
-to-Object
转换器
Spring Integration 还提供了 Object
到 Map
和 Map
到 Object
的转换器,它们使用 JSON 来序列化和反序列化对象图。对象层次结构被内省到最原始的类型(String
、int
等)。通往这种类型的路径是用 SpEL 描述的,这将成为转换后的 Map
中的 key
。原始类型成为值。
考虑以下示例:
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
}
前面示例中的两个类被转换为以下 Map
:
{person.name=George, person.child.name=Jenna, person.child.nickNames[0]=Jen ...}
基于 JSON 的 Map
让你可以描述对象结构而不共享实际的类型,这使得你可以在保持结构不变的情况下,将对象图还原和重建为不同类型的对象图。
例如,前面的结构可以使用 Map
-to-Object
转换器恢复为以下对象图:
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
}
如果你需要创建一个“结构化”的映射,你可以提供 flatten
属性。默认值是 'true'。如果你将其设置为 'false',结构将是一个 Map
的 Map
对象。
考虑以下示例:
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
}
前面示例中的两个类被转换为以下 Map
:
{name=George, child={name=Jenna, nickNames=[Bimbo, ...]}}
要配置这些转换器,Spring Integration 提供了相应的 XML 组件和 Java DSL 工厂:
- 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"/>
你也可以将 flatten
属性设置为 false,如下所示:
- 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 提供了 Map-to-Object 的 XML 命名空间支持,Java DSL 工厂有 fromMap()
方法,如下例所示:
- 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"/>
或者,你可以使用 ref
属性和原型作用域的 bean,如下例所示:
- 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"/>
'ref' 和 'type' 属性是互斥的。另外,如果你使用 'ref' 属性,则必须指向一个 'prototype' 作用域的 bean。否则,将抛出一个 BeanCreationException
。
从 5.0 版本开始,您可以为 ObjectToMapTransformer
提供自定义的 JsonObjectMapper
— 用于当您需要日期或空集合的特殊格式(以及其他用途)。有关 JsonObjectMapper
实现的更多信息,请参阅 JSON 转换器。
流变换器
StreamTransformer
将 InputStream
负载转换为 byte[]
(如果提供了 charset
,则转换为 String
)。
以下示例展示了如何在 XML 中使用 stream-transformer
元素:
- 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 -->
以下示例展示了如何使用 StreamTransformer
类和 @Transformer
注解在 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 转换器
Spring Integration 提供了对象到 JSON 和 JSON 到对象的转换器。以下示例对显示了如何在 XML 中声明它们:
<int:object-to-json-transformer />
<int:json-to-object-transformer type="com.example.MyClass" />
<int:object-to-json-transformer input-channel="objectMapperInput"/>
<int:json-to-object-transformer input-channel="objectMapperInput"
type="foo.MyDomainObject"/>
默认情况下,前面列表中的转换器使用普通的 JsonObjectMapper
。它是基于类路径中的实现。你可以提供自己的自定义 JsonObjectMapper
实现,并带有适当的选项或基于所需的库(例如 GSON),如下例所示:
<int:json-to-object-transformer input-channel="objectMapperInput"
type="something.MyDomainObject" object-mapper="customObjectMapper"/>
从 3.0 版本开始,object-mapper
属性引用了一个新的策略接口 JsonObjectMapper
的实例。这种抽象允许多个 JSON 映射器的实现被使用。提供了一个包装 Jackson 2 的实现,并且会在类路径上检测其版本。相应的类是 Jackson2JsonObjectMapper
。
你可能希望考虑使用 FactoryBean
或工厂方法来创建具有所需特征的 JsonObjectMapper
。以下示例展示了如何使用此类工厂:
public class ObjectMapperFactory {
public static Jackson2JsonObjectMapper getMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
return new Jackson2JsonObjectMapper(mapper);
}
}
下面的示例展示了如何在 XML 中做同样的事情:
<bean id="customObjectMapper" class="something.ObjectMapperFactory"
factory-method="getMapper"/>
从 2.2 版本开始,object-to-json-transformer
默认将 content-type
标头设置为 application/json
,前提是输入消息尚未包含该标头。
如果您希望将 content-type
标头设置为其他值或明确覆盖任何现有标头的值(包括 application/json
),请使用 content-type
属性。如果您希望抑制标头的设置,请将 content-type
属性设置为空字符串 (""
)。这样做会导致消息没有 content-type
标头,除非输入消息中已经存在该标头。
从 3.0 版本开始,ObjectToJsonTransformer
会添加反映源类型的头信息到消息中。类似地,JsonToObjectTransformer
在将 JSON 转换为对象时可以使用这些类型头信息。这些头信息在 AMQP 适配器中进行了映射,因此它们与 Spring-AMQP JsonMessageConverter 完全兼容。
这使得以下流程无需任何特殊配置即可工作:
-
…→amqp-outbound-adapter---→
-
---→amqp-inbound-adapter→json-to-object-transformer→…
其中,出站适配器配置了
JsonMessageConverter
,而入站适配器使用默认的SimpleMessageConverter
。 -
…→object-to-json-transformer→amqp-outbound-adapter---→
-
---→amqp-inbound-adapter→…
其中,出站适配器配置了
SimpleMessageConverter
,而入站适配器使用默认的JsonMessageConverter
。 -
…→object-to-json-transformer→amqp-outbound-adapter---→
-
---→amqp-inbound-adapter→json-to-object-transformer→
其中,两个适配器都配置了
SimpleMessageConverter
。
在使用标题来确定类型时,你不应提供 class
属性,因为它的优先级高于标题。
除了 JSON Transformers,Spring Integration 还提供了一个内置的 #jsonPath
SpEL 函数,用于在表达式中使用。有关更多信息,请参阅 Spring 表达式语言 (SpEL)。
自从 3.0 版本以来,Spring Integration 还提供了一个内置的 #xpath
SpEL 函数,可用于表达式中。有关更多信息,请参阅 #xpath SpEL 函数。
从 4.0 版本开始,ObjectToJsonTransformer
支持 resultType
属性,用于指定节点的 JSON 表示。结果节点树表示取决于提供的 JsonObjectMapper
的实现。默认情况下,ObjectToJsonTransformer
使用 Jackson2JsonObjectMapper
并将对象转换为节点树的操作委托给 ObjectMapper#valueToTree
方法。节点的 JSON 表示在下游消息流使用 SpEL 表达式访问 JSON 数据的属性时,为使用 JsonPropertyAccessor
提供了效率。有关更多信息,请参阅属性访问器。
从 5.1 版开始,可以将 resultType
配置为 BYTES
,以生成带有 byte[]
负载的消息,这在与使用此类数据类型的下游处理器协同工作时非常方便。
从 5.2 版本开始,JsonToObjectTransformer
可以配置一个 ResolvableType
,以便在使用目标 JSON 处理器反序列化时支持泛型。此外,此组件现在会首先检查请求消息头中是否存在 JsonHeaders.RESOLVABLE_TYPE
或 JsonHeaders.TYPE_ID
,否则将回退到配置的类型。ObjectToJsonTransformer
现在也会根据请求消息的有效负载填充一个 JsonHeaders.RESOLVABLE_TYPE
头,以应对任何可能的下游场景。
从 5.2.6 版本开始,JsonToObjectTransformer
可以提供一个 valueTypeExpression
来解析一个 ResolvableType
,用于在运行时将 JSON 转换为请求消息的有效负载。默认情况下,它会在请求消息中咨询 JsonHeaders
。如果此表达式返回 null
或者构建 ResolvableType
时抛出 ClassNotFoundException
,转换器将回退到提供的 targetType
。这种逻辑以表达式的形式存在,是因为 JsonHeaders
可能没有实际的类值,而是一些类型 ID,这些类型 ID 必须根据某些外部注册表映射到目标类。
Apache Avro 转换器
版本 5.2 添加了简单的转换器,用于在 Apache Avro 之间进行转换。
它们的不复杂之处在于没有模式注册表;转换器只是使用从 Avro 模式生成的 SpecificRecord
实现中嵌入的模式。
发送到 SimpleToAvroTransformer
的消息必须具有实现 SpecificRecord
的有效负载;该转换器可以处理多种类型。SimpleFromAvroTransformer
必须配置一个 SpecificRecord
类,该类用作反序列化的默认类型。您还可以使用 setTypeExpression
方法指定一个 SpEL 表达式来确定要反序列化的类型。默认的 SpEL 表达式是 headers[avro_type]
(AvroHeaders.TYPE
),默认情况下,它由 SimpleToAvroTransformer
使用源类的全限定类名填充。如果表达式返回 null
,则使用 defaultType
。
SimpleToAvroTransformer
还有一个 setTypeExpression
方法。这允许解耦生产者和消费者,发送者可以将头设置为表示类型的某个令牌,然后消费者将该令牌映射到一个类型。
Protocol Buffers 变换器
版本 6.1 增加了对从 Protocol Buffers 数据内容转换的支持。
ToProtobufTransformer
将 com.google.protobuf.Message
消息负载转换为原生字节数组或 json 文本负载。application/x-protobuf
内容类型(默认使用)生成字节数组输出负载。如果内容类型是 application/json
,请添加 com.google.protobuf:protobuf-java-util
(如果在类路径上找到),然后输出将是文本 json 负载。如果未设置内容类型头,ToProtobufTransformer
默认为 application/x-protobuf
。
FromProtobufTransformer
将字节数组或文本 protobuf 负载(取决于内容类型)转换回 com.google.protobuf.Message
实例。FromProtobufTransformer
应该显式指定一个预期的类类型(使用 setExpectedType
方法),或者使用 SpEL 表达式通过 setExpectedTypeExpression
方法来确定反序列化的类型。默认的 SpEL 表达式是 headers[proto_type]
(ProtoHeaders.TYPE
),它由 ToProtobufTransformer
使用源 com.google.protobuf.Message
类的完全限定类名填充。
例如,编译以下 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;
}
将生成一个新的 org.example.MyMessageClass
类。
然后使用:
:
// 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);
使用注解配置 Transformer
你可以将 @Transformer
注解添加到期望 Message
类型或消息有效负载类型的方法上。返回值的处理方式与前面描述的 在描述 <transformer> 元素的部分中所述完全相同。以下示例展示了如何使用 @Transformer
注解将一个 String
转换为 Order
:
@Transformer
Order generateOrder(String productId) {
return new Order(productId);
}
Transformer 方法也可以接受 @Header
和 @Headers
注解,如在 注解支持 中所记录的。以下示例显示了如何使用 @Header
注解:
@Transformer
Order generateOrder(String productId, @Header("customerName") String customer) {
return new Order(productId, customer);
}
另见使用注解建议端点。
Header 过滤器
有时,你的转换用例可能简单到只需要移除几个标题。对于这样的用例,Spring Integration 提供了一个标题过滤器,它允许你指定一些应该从输出消息中移除的标题名称(例如,由于安全原因移除标题或仅需临时使用的值)。基本上,标题过滤器是标题增强器的相反。后者在 Header Enricher 中讨论。以下示例定义了一个标题过滤器:
- 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"/>
如您所见,配置标题过滤器非常简单。它是一个典型的端点,具有输入和输出通道以及一个 header-names
属性。该属性接受需要移除的标题名称(如果有多个,则用逗号分隔)。因此,在前面的例子中,名为 'lastName' 和 'state' 的标题在 outbound 消息中不存在。
基于编解码器的 Transformer
参见 Codec。