Transformer

消息转换器在启用消息生成者和消息使用者的松散耦合方面发挥着非常重要的作用。你可以添加那些组件之间的转换器,而不是要求每个消息生成组件知道下一个使用程序期望的类型。通用的转换器(例如将 String 转换为 XML 文档的转换器)也高度可重复使用。 对于某些系统,最好提供 canonical data model,但 Spring 集成的总体理念是不需要任何特定格式。相反,为了实现最大的灵活性,Spring 集成旨在为扩展提供尽可能简单的模型。与其他端点类型一样,在 XML 或 Java 注释中使用声明性配置,可以将简单的 POJO 调整为消息转换器的角色。本章的其余部分介绍这些配置选项。

为了最大限度地提高灵活性,Spring 不要求基于 XML 的消息负载。但是,如果这确实是您应用程序的正确选择,则该框架确实提供了一些方便的转换器来处理基于 XML 的负载。有关这些转换器的更多信息,请参阅 XML Support - Dealing with XML Payloads

Configuring a Transformer with Java and other DSLs

对于简单的 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 的更多信息:

Configuring a Transformer with XML

<transformer> 元素用于创建消息转换端点。除了 input-channeloutput-channel 属性外,它还要求提供 ref 属性。ref 可以指向一个对象,该对象在一个方法上包含 @Transformer 注释(参见 Configuring a Transformer with Annotations),或者可以与 method 属性中提供的显式方法名称值结合使用。

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

如果自定义转换器处理程序实现可以在其他 <transformer> 定义中重复使用,通常建议使用 ref 属性。但是,如果自定义转换器处理程序实现应限定到 <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 and Spring Expression Language (SpEL)

与路由器、聚合器和其他组件一样,从 Spring 集成 2.0 开始,当转换逻辑相对简单时,转换器也可以受益于 SpEL support。以下示例展示了如何使用 SpEL 表达式:

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

前一个示例在不编写自定义转换器的情况下转换有效负载。我们的有效负载(假设是一个 String)被大写化、与当前时间戳串联并应用了一些格式。

Common Transformers

Spring Integration 提供了一些转换器实现。

Object-to-String Transformer

因为使用 ObjecttoString() 表示相当常见,所以 Spring Integration 提供了 ObjectToStringTransformer(也请参阅 Transformers 工厂),其中输出是带有字符串 payloadMessage。该 String 是对入站消息有效负载调用 toString() 操作的结果。以下示例演示如何声明 object-to-string 转换器的实例:

  • 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 能够记录消息负载。有关更多详情,请参阅 Wire Tap

object-to-string 转换器非常简单。它对入站有效负载调用 toString()。从 Spring Integration 3.0 开始,此规则有两个例外:

  • 如果有效负载为 char[],则它会调用 new String(payload)

  • 如果有效负载为 byte[],则它会调用 new String(payload, charset),其中 charset 默认为 UTF-8。可以通过在转换器上提供 charset 属性来修改 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 序列化,但你可以使用 serializerdeserializer 属性分别提供 Spring SerializerDeserializer 策略的实现。另请参阅 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 and Map-to-Object Transformers

Spring Integration 还提供了 ObjectMapMapObject 转换器,这些转换器使用 JSON 来序列化和反序列化对象图。该对象层次结构会内省到最原始类型(Stringint 等)。此类型的路径用 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 让你能够描述对象结构,而无需共享实际类型,只要你保持结构,这就会让你能够将对象图还原和重建到一个类型不同的对象图中。

例如,可以通过使用 MapObject 转换器将前一个结构还原到以下对象图:

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 版开始,您可以使用自定义 JsonObjectMapper`提供 `ObjectToMapTransformer——当您需要为日期或空集合的空值(以及其他用途)设置特殊格式时。有关 `JsonObjectMapper`实现的更多信息,请参阅 JSON Transformers

Stream Transformer

StreamTransformerInputStream 有效负载转换为 byte[](或 String,如果提供了 charset)。

以下示例展示了如何在 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 -->

以下示例展示了如何在 Java 中使用 StreamTransformer 类和 @Transformer 注解配置流转换器:

@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 提供了 Object-to-JSON 和 JSON-to-Object 转换器。以下示例展示了如何在 XML 中声明它们:

<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 添加标题,反映消息的源类型。同样,在将 JSON 转换为对象时,JsonToObjectTransformer 可以使用这些类型标题。这些标题映射在 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 转换器之外,Spring Integration 还提供了内置 #jsonPath SpEL 函数,用于表达式。有关更多信息,请参阅 Spring Expression Language (SpEL)

自 3.0 版本以来,Spring Integration 为表达式使用内置了 #xpath SpEL 函数。有关更多信息,请参阅 #xpath SpEL Function

从 4.0 版开始,ObjectToJsonTransformer`支持 `resultType`属性,以指定节点 JSON 表示。结果节点树表示取决于提供的 `JsonObjectMapper`的实现。默认情况下,`ObjectToJsonTransformer`使用 `Jackson2JsonObjectMapper,并将对象转换为节点树的任务委派给 `ObjectMapper#valueToTree`方法。当下游消息流使用能够访问 JSON 数据属性的 SpEL 表达式时,节点 JSON 表示提供了便于使用 `JsonPropertyAccessor`的效率。有关更多信息,请参阅 Property Accessors

从 5.1 版本开始,resultType 可以配置为 BYTES,以便生成具有 byte[] 有效负载的消息,以方便处理此数据类型的下游处理程序。

从 5.2 版开始,JsonToObjectTransformer 可以配置为 ResolvableType,以支持在使用目标 JSON 处理器进行反序列化期间的泛型。此外,此组件现在首先对请求消息头进行咨询,以查找是否存在 JsonHeaders.RESOLVABLE_TYPEJsonHeaders.TYPE_ID,否则回退到已配置的类型。ObjectToJsonTransformer 现在还会根据任何可能的下游方案,针对请求消息有效负载填充 JsonHeaders.RESOLVABLE_TYPE 头。

从 5.2.6 版开始,JsonToObjectTransformer 可以提供 valueTypeExpression,以解决请求消息中要从 JSON 转换的有效负载的 ResolvableType。默认情况下,它会在请求消息中查询 JsonHeaders。如果此表达式返回 nullResolvableType 构建抛出 ClassNotFoundException,则转换器会回退到提供的 targetType。此逻辑以表达式的形式存在,因为 JsonHeaders 可能是没有实际类值的类型 ID,这些类型 ID 必须映射到目标类,具体视某些外部注册表而定。

Apache Avro Transformers

5.2 版添加了用于转换到/从 Apache Avro 的简单转换器。

这些转换器比较简单,因为没有模式注册表;转换器仅仅使用从 Avro 模式生成的 SpecificRecord 实现中嵌入的模式。

发送到 SimpleToAvroTransformer 的消息必须具有实现 SpecificRecord 的有效负载;转换器可以处理多种类型。必须使用作为默认类型用于反序列化的 SpecificRecord 类配置 SimpleFromAvroTransformer。您还可以指定一个 SpEL 表达式,以确定使用 setTypeExpression 方法进行反序列化的类型。默认 SpEL 表达式为 headers[avro_type] (AvroHeaders.TYPE),它默认由 SimpleToAvroTransformer 使用源类的完全限定类名填充。如果表达式返回 null,则使用 defaultType

SimpleToAvroTransformer 还具有 setTypeExpression 方法。这允许解除生产者和消费者的耦合,其中发送方可以将头设置为表示类型的某个令牌,然后使用者再将该令牌映射到类型。

Protocol Buffers Transformers

6.1 版本增加了对转换 Protocol Buffers 数据内容的支持。

ToProtobufTransformercom.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);

Configuring a Transformer with Annotations

可以将 @Transformer 注解添加到需要 Message 类型或消息有效负载类型的。返回值的处理方式与前面 [在描述 <transformer> 元素的章节中transformer-namespace] 中描述的一模一样。以下示例显示如何使用 @Transformer 注解将 String 转换成 Order

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

Annotation Support 中所述,转换器方法还可以接受 @Header@Headers 注释。以下示例展示了如何使用 @Header 注释:

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

Header Filter

有时候,您的转换用例可能仅仅是删除一些标头。对于这类用例,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”的头在出站消息中不存在。

Codec-Based Transformers

有关更多信息,请参阅 Codec