# 消息转换

# 消息转换

# 转换器

消息转换器在实现消息生产者和消息消费者之间的松耦合方面发挥着非常重要的作用。你可以在这些组件之间添加转换器,而不是要求每个产生消息的组件都知道下一个使用者期望的类型。通用的转换器,例如将String转换为 XML 文档的转换器,也是高度可重用的。

对于某些系统,最好是提供规范数据模型 (opens new window),但是 Spring 集成的一般原理是不需要任何特定的格式。相反,为了获得最大的灵活性, Spring 集成旨在为扩展提供尽可能简单的模型。与其他端点类型一样,在 XML 或 Java 注释中使用声明式配置使简单的 POJO 能够适应消息转换器的角色。本章的其余部分将描述这些配置选项。

为了最大限度地提高灵活性, Spring 不需要基于 XML 的消息有效负载。
不过,如果这确实是你的应用程序的正确选择,那么该框架确实为处理基于 XML 的有效负载提供了一些方便的转换器。
有关这些转换器的更多信息,请参见XML 支持-处理 XML 有效负载

# 使用 XML 配置转换器

<transformer>元素用于创建消息转换端点。除了input-channeloutput-channel属性外,它还需要一个ref属性。ref可以指向单个方法上包含@Transformer注释的对象(参见用注解配置转换器),也可以与method属性中提供的显式方法名值结合。

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

如果定制的 Transformer 处理程序实现可以在其他<transformer>定义中重用,则通常建议使用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 Integration2.0 中,消息转换器的转换方法不再能够返回null。返回null将导致一个异常,因为消息转换器应该始终被期望将每个源消息转换为有效的目标消息。换句话说,消息转换器不应该被用作消息过滤器,因为它有一个专用的<filter>选项。但是,如果你确实需要这种类型的行为(其中组件可能会返回null,这不应被视为错误),则可以使用服务激活器。其requires-reply值默认为false,但可以设置为true,以便为null抛出异常返回值,就像转换器一样。

# 转换器与 Spring 表达式语言

与路由器、聚合器和其他组件一样,在 Spring Integration2.0 中,只要转换逻辑相对简单,转换器也可以从SPEL 支持 (opens new window)中受益。下面的示例展示了如何使用 SPEL 表达式:

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

前面的示例在不编写自定义转换器的情况下转换有效负载。我们的有效负载(假定为String)是上层的,与当前的时间戳连接在一起,并应用了一些格式。

# 通用转换器

Spring 集成提供了一些转换器实现方式。

# 对象到字符串转换器

因为使用toString()表示Object是相当常见的, Spring 集成提供了一个ObjectToStringTransformer,其输出是带有字符串payloadMessage。这个String是在入站消息的有效负载上调用toString()操作的结果。下面的示例展示了如何声明对象到字符串转换器的实例:

<int:object-to-string-transformer input-channel="in" output-channel="out"/>

此转换器的一个潜在用途是向file命名空间中的“出站通道适配器”发送一些任意对象。默认情况下,该通道适配器仅支持String、字节数组或java.io.File有效负载,在适配器处理必要的转换之前立即添加此转换器。只要toString()调用的结果是你想要写入到文件中的内容,那么它就可以正常工作。否则,你可以使用前面显示的通用“Transformer”元素来提供基于 POJO 的自定义转换器。

调试时,此转换器通常不是必需的,因为logging-channel-adapter能够记录消息有效负载。
有关更多详细信息,请参见Wire Tap
Object-to-String Transformer 非常简单。它在入站有效负载上调用。自 Spring Integration3.0 以来,该规则有两个例外:* 如果有效负载是,则调用。如果有效负载是,它调用new String(payload, charset),其中charset是默认的 UTF-8。
charset可以通过在转换器上提供字符集属性来修改

以获得更复杂的操作(例如在运行时动态地选择字符集),你可以使用基于 spel 表达式的转换器,如下例所示:

<br/><int:transformer input-channel="in" output-channel="out"<br/> expression="new java.lang.String(payload, headers['myCharset']" /><br/>

如果需要将Object序列化为字节数组,或者将字节数组反序列化为Object, Spring 集成提供了对称的序列化转换器。这些默认情况下使用标准的 Java 序列化,但是可以通过分别使用serializerdeserializer属性提供 Spring SerializerDeserializer策略的实现。下面的示例展示了如何使用 Spring 的序列化器和反序列化器:

<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-MapMap-to-Object转换器

Spring 集成还提供了Object-to-MapMap-to-Object转换器,它们使用 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允许你在不共享实际类型的情况下描述对象结构,这允许你将对象图恢复并重建为不同类型的对象图,只要你保持该结构即可。

例如,可以通过使用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”,则结构是MapMap对象。

考虑以下示例:

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 为对象到映射提供了名称空间支持,如下例所示:

<int:object-to-map-transformer input-channel="directInput" output-channel="output"/>

还可以将flatten属性设置为 false,如下所示:

<int:object-to-map-transformer input-channel="directInput" output-channel="output" flatten="false"/>

Spring 集成为映射到对象提供了名称空间支持,如下例所示:

<int:map-to-object-transformer input-channel="input"
                         output-channel="output"
                         type="org.something.Person"/>

另外,可以使用ref属性和原型范围 Bean,如下例所示:

<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”属性,则必须指向 Bean 范围内的“prototype”。
否则,将抛出一个BeanCreationException

从版本 5.0 开始,你可以为ObjectToMapTransformer提供自定义的JsonObjectMapper——当你需要为空集合的日期或空集合(以及其他用途)提供特殊格式时。有关JsonObjectMapper实现的更多信息,请参见JSON 转换器

# 流转换器

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

下面的示例展示了如何在 XML 中使用stream-transformer元素:

<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 集成提供了对象到 JSON 和 JSON 到对象转换器。以下两个示例展示了如何用 XML 声明它们:

<int:object-to-json-transformer input-channel="objectMapperInput"/>
<int:json-to-object-transformer input-channel="objectMapperInput"
    type="foo.MyDomainObject"/>

默认情况下,前一个列表中的 Transformers 使用的是 vanillaJsonObjectMapper。它是基于来自 Classpath 的一种实现方式。你可以为自己的自定义JsonObjectMapper实现提供适当的选项,或者基于所需的库(例如 GSON),如下例所示:

<int:json-to-object-transformer input-channel="objectMapperInput"
    type="something.MyDomainObject" object-mapper="customObjectMapper"/>
从版本 3.0 开始,object-mapper属性引用了一个新策略接口的实例:JsonObjectMapper
此抽象允许使用 JSON 映射程序的多个实现。
提供了 wrapsJackson 2 (opens new window)的实现,并在 Classpath 上检测到该版本。
类分别是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](https://DOCS. Spring.io/ Spring-AMQP/API/)完全兼容。

这使得以下流程可以在不进行任何特殊配置的情况下工作:

  • …​→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属性,因为它优先于头。

Spring 除了 JSON Transformers 之外,Integration 还提供了一个用于表达式的内置#jsonPathspel 函数。有关更多信息,请参见Spring Expression Language (SpEL)

自版本 3.0 以来, Spring 集成还提供了用于表达式的内置#xpathspel 函数。有关更多信息,请参见#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_TYPEJsonHeaders.TYPE_ID,否则将返回到配置的类型。对于任何可能的下游场景,ObjectToJsonTransformer现在还基于请求消息有效负载填充JsonHeaders.RESOLVABLE_TYPE头。

从版本 5.2.6 开始,JsonToObjectTransformer可以提供一个valueTypeExpression来解析一个ResolvableType,以便在运行时根据请求消息从 JSON 转换有效负载。默认情况下,它在请求消息中查询JsonHeaders。如果这个表达式返回nullResolvableTypebuilding 抛出一个ClassNotFoundException,则转换器返回到所提供的targetType。这个逻辑以表达式的形式存在,因为JsonHeaders可能没有真正的类值,而是一些类型 ID,这些类型 ID 必须根据某些外部注册中心映射到目标类。

# Apache AVROTransformers

版本 5.2 增加了简单的转换器,以转换到/从 Apache AVRO。

它们并不复杂,因为没有模式注册中心;转换器只使用嵌入在由 AVRO 模式生成的SpecificRecord实现中的模式。

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

SimpleToAvroTransformer也有一个setTypeExpression方法。这允许生产者和消费者分离,在这种情况下,发送者可以将头设置为表示类型的某个令牌,然后消费者将该令牌映射到类型。

# 配置带有注释的转换器

你可以将@Transformer注释添加到需要Message类型或消息有效负载类型的方法中。返回值的处理方式与前面(在描述<transformer>元素的一节中)描述的完全相同。下面的示例展示了如何使用@Transformer注释将String转换为Order:

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

Transformer 方法也可以接受@Header@Headers注释,如[Annotation Support](./configuration.html#annotations)中所述。以下示例展示了如何使用@Header注释:

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

另见使用注释为端点提供建议

# 页眉过滤器

有时,你的转换用例可能很简单,只需删除几个标题即可。对于这样的用例, Spring Integration 提供了一个头过滤器,它允许你指定应该从输出消息中删除的某些头名称(例如,出于安全原因删除头,或者仅暂时需要的值)。基本上,header 过滤器与 header enricher 相反。后者在页眉 Enricher中讨论。下面的示例定义了一个头过滤器:

<int:header-filter input-channel="inputChannel"
		output-channel="outputChannel" header-names="lastName, state"/>

正如你所看到的,头过滤器的配置非常简单。它是一个典型的端点,具有输入和输出通道以及header-names属性。该属性接受需要删除的标头的名称(如果有多个标头,则用逗号分隔)。因此,在前面的示例中,出站消息中不存在名为“lastname”和“state”的头。

# 基于编解码器的转换器

Codec

# 内容更丰富

有时,你可能需要使用比目标系统提供的更多信息来增强请求。数据收集器 (opens new window)模式描述了各种场景以及允许你满足此类需求的组件。

Spring IntegrationCore模块包括两个增强器:

它还包括三个特定于适配器的头增强程序:

有关这些适配器的更多信息,请参阅本参考手册中与适配器相关的部分。

有关表达式支持的更多信息,请参见Spring Expression Language (SpEL)

# 页眉 Enricher

如果只需要向消息添加头,而头不是由消息内容动态确定的,那么引用 Transformer 的自定义实现可能会导致过度使用。出于这个原因, Spring 集成为 Header Enricher 模式提供了支持。它是通过<header-enricher>元素公开的。下面的示例展示了如何使用它:

<int:header-enricher input-channel="in" output-channel="out">
    <int:header name="foo" value="123"/>
    <int:header name="bar" ref="someBean"/>
</int:header-enricher>

Header Enricher 还提供了有用的子元素来设置众所周知的头名称,如下例所示:

<int:header-enricher input-channel="in" output-channel="out">
    <int:error-channel ref="applicationErrorChannel"/>
    <int:reply-channel ref="quoteReplyChannel"/>
    <int:correlation-id value="123"/>
    <int:priority value="HIGHEST"/>
    <routing-slip value="channel1; routingSlipRoutingStrategy; request.headers[myRoutingSlipChannel]"/>
    <int:header name="bar" ref="someBean"/>
</int:header-enricher>

前面的配置显示,对于已知的头(例如errorChannelcorrelationIdpriorityreplyChannelrouting-slip,以及其他),而不是使用泛型<header>子元素,其中必须同时提供头’名称’和’值’,你可以使用方便的子元素直接设置这些值。

从版本 4.1 开始,Header Enricher 提供了一个routing-slip子元素。有关更多信息,请参见布线滑移

# POJO 支持

通常,头文件的值不能静态地定义,而必须根据消息中的某些内容动态地确定。这就是为什么 Header Enricher 允许你通过使用refmethod属性来指定 Bean 引用的原因。指定的方法计算标头值。考虑以下配置和具有修改String的方法的 Bean:

<int:header-enricher input-channel="in" output-channel="out">
    <int:header name="something" method="computeValue" ref="myBean"/>
</int:header-enricher>

<bean id="myBean" class="thing1.thing2.MyBean"/>
public class MyBean {

    public String computeValue(String payload){
        return payload.toUpperCase() + "_US";
    }
}

还可以将 POJO 配置为内部 Bean,如下例所示:

<int:header-enricher  input-channel="inputChannel" output-channel="outputChannel">
    <int:header name="some_header">
        <bean class="org.MyEnricher"/>
    </int:header>
</int:header-enricher>

你可以类似地指向一个 Groovy 脚本,如下例所示:

<int:header-enricher  input-channel="inputChannel" output-channel="outputChannel">
    <int:header name="some_header">
        <int-groovy:script location="org/SampleGroovyHeaderEnricher.groovy"/>
    </int:header>
</int:header-enricher>
# spel 支持

在 Spring Integration2.0 中,我们引入了Spring Expression Language (SpEL) (opens new window)的便利,以帮助配置许多不同的组件。标题 Enricher 就是其中之一。再看看前面显示的 POJO 示例。你可以看到,确定标头值的计算逻辑非常简单。一个自然的问题是:“有没有更简单的方法来实现这一点?”这就是 SPEL 展现其真正力量的地方。考虑以下示例:

<int:header-enricher input-channel="in" output-channel="out">
    <int:header name="foo" expression="payload.toUpperCase() + '_US'"/>
</int:header-enricher>

通过在这种简单的情况下使用 SPEL,你不再需要提供一个单独的类并在应用程序上下文中对其进行配置。你所需要做的就是将expression属性配置为一个有效的 SPEL 表达式。“payload”和“headers”变量绑定到 SPEL 评估上下文,从而使你能够完全访问传入的消息。

# 使用 Java 配置来配置标题 Enricher

下面的两个示例展示了如何使用 Java 配置来增强报头:

@Bean
@Transformer(inputChannel = "enrichHeadersChannel", outputChannel = "emailChannel")
public HeaderEnricher enrichHeaders() {
    Map<String, ? extends HeaderValueMessageProcessor<?>> headersToAdd =
            Collections.singletonMap("emailUrl",
                      new StaticHeaderValueMessageProcessor<>(this.imapUrl));
    HeaderEnricher enricher = new HeaderEnricher(headersToAdd);
    return enricher;
}

@Bean
@Transformer(inputChannel="enrichHeadersChannel", outputChannel="emailChannel")
public HeaderEnricher enrichHeaders() {
    Map<String, HeaderValueMessageProcessor<?>> headersToAdd = new HashMap<>();
    headersToAdd.put("emailUrl", new StaticHeaderValueMessageProcessor<String>(this.imapUrl));
    Expression expression = new SpelExpressionParser().parseExpression("payload.from[0].toString()");
    headersToAdd.put("from",
               new ExpressionEvaluatingHeaderValueMessageProcessor<>(expression, String.class));
    HeaderEnricher enricher = new HeaderEnricher(headersToAdd);
    return enricher;
}

第一个示例添加了一个文字头。第二个示例添加了两个头,一个文字头和一个基于 SPEL 表达式的头。

# 使用 Java DSL 配置 header Enricher

下面的示例展示了 Header Enricher 的 Java DSL 配置:

@Bean
public IntegrationFlow enrichHeadersInFlow() {
    return f -> f
                ...
                .enrichHeaders(h -> h.header("emailUrl", this.emailUrl)
                                     .headerExpression("from", "payload.from[0].toString()"))
                .handle(...);
}
# 报头通道注册表

从 Spring Integration3.0 开始,一个新的子元素<int:header-channels-to-string/>可用。它没有任何属性。这个新的子元素将现有的replyChannelerrorChannel头(当它们是MessageChannel时)转换为String,并将通道存储在注册表中,以便以后在需要发送回复或处理错误时进行解析。这对于可能丢失消息头的情况很有用——例如,将消息序列化到消息存储中或通过 JMS 传输消息时。如果标题不存在,或者它不是MessageChannel,则不进行任何更改。

使用此功能需要存在HeaderChannelRegistry Bean。默认情况下,该框架创建一个DefaultHeaderChannelRegistry,默认过期时间为 60 秒。在此时间之后,通道将从注册表中删除。要更改此行为,请使用idintegrationHeaderChannelRegistry定义 Bean,并使用构造函数参数(以毫秒为单位)配置所需的默认延迟。

从版本 4.1 开始,你可以在<bean/>定义上将一个名为removeOnGet的属性设置为true,并且在第一次使用时立即删除该映射条目。这在大容量环境中以及通道只被使用一次(而不是等待收割者删除它)时可能是有用的。

HeaderChannelRegistry有一个size()方法来确定注册表的当前大小。runReaper()方法取消当前的计划任务并立即运行 Reaper。然后根据当前的延迟将任务安排为再次运行。可以通过获取对注册表的引用来直接调用这些方法,或者可以向控制总线发送带有以下内容的消息:

"@integrationHeaderChannelRegistry.runReaper()"

这个子元素很方便,相当于指定了以下配置:

<int:reply-channel
    expression="@integrationHeaderChannelRegistry.channelToChannelName(headers.replyChannel)"
    overwrite="true" />
<int:error-channel
    expression="@integrationHeaderChannelRegistry.channelToChannelName(headers.errorChannel)"
    overwrite="true" />

从版本 4.1 开始,你现在可以重写注册表配置的收割者延迟,以便至少在指定的时间内保留通道映射,而不考虑收割者延迟。下面的示例展示了如何做到这一点:

<int:header-enricher input-channel="inputTtl" output-channel="next">
    <int:header-channels-to-string time-to-live-expression="120000" />
</int:header-enricher>

<int:header-enricher input-channel="inputCustomTtl" output-channel="next">
    <int:header-channels-to-string
        time-to-live-expression="headers['channelTTL'] ?: 120000" />
</int:header-enricher>

在第一种情况下,每个头通道映射的可用时间为两分钟。在第二种情况下,在消息头中指定了生存时间,如果没有消息头,则使用 Elvis 操作符使用两分钟。

# 有效载荷 Enricher

在某些情况下,正如前面所讨论的,header enricher 可能是不够的,并且有效负载本身可能需要用额外的信息来丰富。例如,进入 Spring 集成消息传递系统的订单消息必须基于提供的客户编号查找订单的客户,然后用该信息丰富原始有效负载。

Spring 集成 2.1 引入了有效负载 Enricher。有效负载 Enricher 定义了一个端点,该端点将Message传递给公开的请求通道,然后期望得到一条回复消息。然后,回复消息将成为表达式求值的根对象,以丰富目标有效负载。

有效负载 Enricher 通过enricher元素提供完整的 XML 命名空间支持。为了发送请求消息,有效负载 Enricher 具有request-channel属性,该属性允许你将消息分派到请求通道。

基本上,通过定义请求通道,有效负载 Enricher 充当网关,等待发送到请求通道的消息返回。然后,Enricher 使用回复消息提供的数据来增加消息的有效负载。

当向请求通道发送消息时,你还可以选择使用request-payload-expression属性仅发送原始有效负载的一个子集。

有效载荷的充实是通过 SPEL 表达式进行配置的,提供了最大程度的灵活性。因此,你不仅可以使用来自回复通道的Message的直接值来丰富有效负载,还可以使用 SPEL 表达式从该消息中提取一个子集或应用其他内联转换,从而进一步操作数据。

如果只需要用静态值来丰富有效负载,则不需要提供request-channel属性。

增强器是转换器的一种变体。
在许多情况下,你可以使用有效负载 Enricher 或通用 Transformer 实现来向消息有效负载添加额外的数据。
你应该熟悉 Spring Integration 提供的所有具有转换能力的组件,并仔细选择语义上最适合你的业务用例的实现。
# 配置

下面的示例显示了有效负载 Enricher 的所有可用配置选项:

<int:enricher request-channel=""                           (1)
              auto-startup="true"                          (2)
              id=""                                        (3)
              order=""                                     (4)
              output-channel=""                            (5)
              request-payload-expression=""                (6)
              reply-channel=""                             (7)
              error-channel=""                             (8)
              send-timeout=""                              (9)
              should-clone-payload="false">                (10)
    <int:poller></int:poller>                              (11)
    <int:property name="" expression="" null-result-expression="'Could not determine the name'"/>   (12)
    <int:property name="" value="23" type="java.lang.Integer" null-result-expression="'0'"/>
    <int:header name="" expression="" null-result-expression=""/>   (13)
    <int:header name="" value="" overwrite="" type="" null-result-expression=""/>
</int:enricher>
1 发送消息以获取用于充实的数据的通道。
可选的。
2 生命周期属性表示是否应该在应用程序上下文启动期间启动此组件。
默认为真。
可选。
3 底层 Bean 定义的 ID,它是EventDrivenConsumerPollingConsumer的可选项。
4 指定当此端点作为订阅服务器连接到某个通道时的调用顺序。
当该通道使用“故障转移”调度策略时,这一点尤其相关。
当该端点本身是具有队列的通道的轮询消费者时,它没有任何作用。
可选的。
5 标识在此端点处理消息后发送消息的消息通道。
可选的。
6 默认情况下,原始消息的有效负载被用作发送到request-channel的有效负载,
通过指定一个 SPEL 表达式作为request-payload-expression属性的值,你可以使用原始有效负载的一个子集,一个标头值,或任何其他可解析的 SPEL 表达式作为发送到请求通道的有效负载的基础。
对于表达式求值,完整的消息作为“根对象”可用。
例如,以下 SPEL 表达式(其中包括)是可能的:payload.somethingheaders.somethingnew java.util.Date()'thing1' + 'thing2'
7 需要回复消息的通道。
这是可选的。
通常,自动生成的临时回复通道就足够了。
可选。
8 如果Exception发生在request-channel的下游,则向其发送ErrorMessage的通道。
这使你能够返回用于丰富的替代对象。
如果未设置,则将Exception抛给调用者。
可选。
9 如果信道可能阻塞,则向信道发送消息时等待的最长时间(以毫秒为单位),
例如,如果队列信道的最大容量已达到,则队列信道可以阻塞,直到有可用的空间为止,
内部,发送超时设置在MessagingTemplate上,并最终在调用MessageChannel上的发送操作时应用。
默认情况下,发送超时设置为’-1’,这会导致MessageChannel上的发送操作无限期地阻塞,这取决于实现。
可选。
10 布尔值,该值指示在将消息发送到请求通道以获取丰富数据之前是否应该克隆实现Cloneable的任何有效负载。
克隆的版本将被用作最终答复的目标有效负载。
默认为false
可选。
11 如果该端点是轮询消费者,则允许你配置消息 poller。
可选的。
12 每个property子元素都提供了一个属性的名称(通过强制name属性)。
该属性应该可以在目标有效负载实例上设置。
正好是valueexpression属性中的一个属性也必须提供——前一个属性对于要设置的文字值和要计算的 SPEL 表达式的文字值。
计算上下文的根对象是从这个 Enricher 启动的流返回的消息——输入消息如果没有请求通道或应用程序上下文(使用@<beanName>.<beanProperty>spel 语法)。
从版本 4.0 开始,当指定value属性时,还可以指定可选的type属性。
当目标是类型化的 setter 方法时,框架会适当地强制该值(只要存在PropertyEditor)来处理转换。
但是,如果目标负载是Map,则该条目将填充该值而不进行转换。
例如,type属性允许你这样做,将包含数字的String转换为目标有效载荷中的Integer值。
从版本 4.1 开始,你还可以指定一个可选的null-result-expression属性。
enricher返回 null 时,将对其进行求值,并返回求值的输出。
13 每个header子元素都提供了消息头的名称(通过强制name属性)。
正好是valueexpression属性中的一个,也必须提供前者以设置文本值计算上下文的根对象是从这个 Enricher 发起的流返回的消息——输入消息如果没有请求通道或应用程序上下文(使用’@<beanName>.<beanProperty>’spel 语法)。
请注意,与<header-enricher>类似,<enricher>元素的header元素具有typeoverwrite属性。但是,与
属性不同的是,默认情况下,overwrite属性是true,以与<enricher>元素的<property>子元素保持一致。
从版本 4.1 开始,你还可以指定一个可选的null-result-expression属性。
enricher返回 null 时,将对其进行求值,并返回评估的输出。
# 示例

本节包含在各种情况下使用有效负载 Enricher 的几个示例。

这里显示的代码示例是 Spring 集成示例项目的一部分。
参见Spring Integration Samples

在下面的示例中,一个User对象作为Message的有效负载传递:

<int:enricher id="findUserEnricher"
              input-channel="findUserEnricherChannel"
              request-channel="findUserServiceChannel">
    <int:property name="email"    expression="payload.email"/>
    <int:property name="password" expression="payload.password"/>
</int:enricher>

User有几个属性,但最初只设置username。Enricher 的request-channel属性被配置为将User传递给findUserServiceChannel

通过隐式设置reply-channel,返回一个User对象,并通过使用property子元素,从应答中提取属性并用于丰富原始有效负载。

# 如何仅将数据的子集传递给请求通道?

当使用request-payload-expression属性时,可以将有效负载的单个属性而不是完整消息传递到请求通道。在下面的示例中,用户名属性被传递到请求通道:

<int:enricher id="findUserByUsernameEnricher"
              input-channel="findUserByUsernameEnricherChannel"
              request-channel="findUserByUsernameServiceChannel"
              request-payload-expression="payload.username">
    <int:property name="email"    expression="payload.email"/>
    <int:property name="password" expression="payload.password"/>
</int:enricher>

请记住,尽管只传递了用户名,但发送到请求通道的结果消息包含完整的MessageHeaders集。

# 如何充实由收集数据组成的有效载荷?

在下面的示例中,传入的不是User对象,而是Map对象:

<int:enricher id="findUserWithMapEnricher"
              input-channel="findUserWithMapEnricherChannel"
              request-channel="findUserByUsernameServiceChannel"
              request-payload-expression="payload.username">
    <int:property name="user" expression="payload"/>
</int:enricher>

Map包含usernameMap 键下的用户名。只有username被传递到请求通道。该回复包含一个完整的User对象,该对象最终被添加到Map键下的user中。

# 在不使用请求通道的情况下,如何使用静态信息丰富有效负载?

下面的示例根本不使用请求通道,而只使用静态值来丰富消息的有效负载:

<int:enricher id="userEnricher"
              input-channel="input">
    <int:property name="user.updateDate" expression="new java.util.Date()"/>
    <int:property name="user.firstName" value="William"/>
    <int:property name="user.lastName"  value="Shakespeare"/>
    <int:property name="user.age"       value="42"/>
</int:enricher>

请注意,“static”一词在这里的用法并不严谨。你仍然可以使用 SPEL 表达式来设置这些值。

# 索赔检查

在前面的部分中,我们介绍了几个内容更丰富的组件,它们可以帮助你处理消息丢失一段数据的情况。我们还讨论了内容过滤,它允许你从消息中删除数据项。然而,有时我们希望暂时隐藏数据。例如,在分布式系统中,我们可能会接收到具有非常大的有效负载的消息。一些间歇性的消息处理步骤可能不需要访问此有效负载,而一些可能只需要访问某些标头,因此通过每个处理步骤携带较大的消息有效负载可能会导致性能下降,可能会产生安全风险,并可能使调试更加困难。

存储在图书馆中 (opens new window)(或 ClaimCheck)模式描述了一种机制,该机制允许你将数据存储在一个众所周知的位置,同时仅维护指向该数据所在位置的指针(ClaimCheck)。你可以将该指针作为新消息的有效负载传递,从而让消息流中的任何组件在需要时立即获得实际数据。这种方法非常类似于认证邮件流程,即你在邮箱中收到一张索赔支票,然后必须去邮局领取你的实际包裹。这也是同样的想法后,在飞机上或在酒店提取行李。

Spring 集成提供了两种类型的索赔检查转换器:

  • 进站索赔检查转换器

  • 出口索赔检查转换器

可以使用方便的基于名称空间的机制来配置它们。

# 进件索赔检查转换器

传入的索赔检查转换器通过将传入的消息存储在其message-store属性标识的消息存储区中来转换该消息。以下示例定义了传入的索赔检查转换器:

<int:claim-check-in id="checkin"
        input-channel="checkinChannel"
        message-store="testMessageStore"
        output-channel="output"/>

在前面的配置中,在input-channel上接收的消息被持久化到用message-store属性标识的消息存储中,并用生成的 ID 进行索引。该 ID 是该消息的索赔检查。索赔检查也成为发送到output-channel的新(转换)消息的有效负载。

现在,假设在某个时刻你确实需要访问实际的消息。你可以手动访问消息存储区并获取消息的内容,或者可以使用相同的方法(创建一个转换器),但现在你可以使用一个传出的 ClaimCheck 转换器将 ClaimCheck 转换为实际的消息。

下面的清单提供了传入的索赔检查转换器的所有可用参数的概述:

<int:claim-check-in auto-startup="true"             (1)
                    id=""                           (2)
                    input-channel=""                (3)
                    message-store="messageStore"    (4)
                    order=""                        (5)
                    output-channel=""               (6)
                    send-timeout="">                (7)
    <int:poller></int:poller>                       (8)
</int:claim-check-in>
1 生命周期属性表示是否应该在应用程序上下文启动期间启动此组件。
它默认为true
此属性在Chain元素内不可用。
可选。
2 标识基础 Bean 定义(MessageTransformingHandler)。
该属性在Chain元素中不可用。
可选。
3 此端点的接收消息通道。
此属性在Chain元素内不可用。
可选。
4 引用MessageStore将由此索赔检查转换器使用。
如果未指定,默认引用是对名为messageStore的 Bean 的引用。
可选的。
5 指定当此端点作为订阅服务器连接到一个通道时调用的顺序。
这在以下情况下特别相关该通道使用failover调度策略。
当该端点本身是具有队列的通道的轮询消费者时,它没有任何作用。
该属性在Chain元素中不可用。
可选。
6 标识由该端点处理后发送消息的消息通道。
该属性在Chain元素中不可用。
可选。
7 指定向输出通道发送回复消息时等待的最长时间(以毫秒为单位)。
默认为-1—无限期地阻塞。
此属性在Chain元素内不可用。
可选。
8 定义一个 poller。
这个元素在Chain元素中是不可用的。
可选的。

# 出口索赔检查转换器

一个传出的索赔检查转换器允许你将具有索赔检查有效负载的消息转换为具有原始内容作为其有效负载的消息。

<int:claim-check-out id="checkout"
        input-channel="checkoutChannel"
        message-store="testMessageStore"
        output-channel="output"/>

在前面的配置中,在input-channel上收到的消息应该有一个索赔检查作为其有效负载。出站索赔检查转换器通过在消息存储中查询由提供的索赔检查标识的消息,将其转换为带有原始有效负载的消息。然后,它将新签出的消息发送到output-channel

下面的清单提供了即将发出的索赔检查转换器的所有可用参数的概述:

<int:claim-check-out auto-startup="true"             (1)
                     id=""                           (2)
                     input-channel=""                (3)
                     message-store="messageStore"    (4)
                     order=""                        (5)
                     output-channel=""               (6)
                     remove-message="false"          (7)
                     send-timeout="">                (8)
    <int:poller></int:poller>                        (9)
</int:claim-check-out>
1 生命周期属性表示是否应该在应用程序上下文启动期间启动此组件。
它默认为true
此属性在Chain元素内不可用。
可选。
2 标识底层 Bean 定义(MessageTransformingHandler)。
该属性在Chain元素中不可用。
可选。
3 此端点的接收消息通道。
此属性在Chain元素内不可用。
可选。
4 引用MessageStore将由此索赔检查转换器使用。
如果未指定,默认引用是对名为messageStore的 Bean 的引用。
可选的。
5 指定当此端点作为订阅服务器连接到一个通道时调用的顺序。
这在以下情况下特别相关该通道使用failover调度策略。
当该端点本身是具有队列的通道的轮询消费者时,它没有任何作用。
该属性在Chain元素中不可用。
可选。
6 标识由该端点处理后发送消息的消息通道。
该属性在Chain元素中不可用。
可选。
7 如果将消息设置为true,则此转换器将从MessageStore中删除该消息。
当只能“声明”一次消息时,此设置是有用的。
它默认为false
可选的。
8 指定在向输出通道发送回复消息时等待的最长时间(以毫秒为单位)。
它默认为-1—无限期地阻塞。
该属性在Chain元素内不可用。
可选。
9 定义一个 poller。
这个元素在Chain元素中是不可用的。
可选的。

# 索赔一次

有时,一条特定的消息必须只声明一次。作为一个类比,考虑处理飞机行李的过程。你在出发时托运行李,到达时认领。一旦行李被认领了,在没有先托运的情况下,就不能再认领了。为了适应这种情况,我们在claim-check-outTransformer 上引入了remove-message布尔属性。默认情况下,此属性设置为false。但是,如果设置为true,则从MessageStore中删除已声明的消息,因此不能再次声明该消息。

此功能在存储空间方面具有影响,特别是在基于内存MapSimpleMessageStore的情况下,在这种情况下,无法删除消息可能最终导致OutOfMemoryException。因此,如果你不希望提出多个索赔,我们建议你将remove-message属性的值设置为true。下面的示例展示了如何使用remove-message属性:

<int:claim-check-out id="checkout"
        input-channel="checkoutChannel"
        message-store="testMessageStore"
        output-channel="output"
        remove-message="true"/>

# 消息存储中的一个单词

尽管我们很少关心索赔检查的细节(只要它们有效),但你应该知道 Spring 集成中实际索赔检查(指针)的当前实现使用 UUID 来确保唯一性。

org.springframework.integration.store.MessageStore是用于存储和检索消息的策略接口。 Spring 集成提供了 IT 的两种方便的实现方式:

  • SimpleMessageStore:一个基于内存的Map实现(缺省的,适合测试)

  • JdbcMessageStore:在 JDBC 上使用关系数据库的实现

# 编解码器

Spring 集成的 4.2 版本引入了Codec抽象。编解码器对byte[]之间的对象进行编码和解码。它们为 Java 序列化提供了一种替代方案。一个优点是,通常,对象不需要实现Serializable。我们提供了一个使用Kryo (opens new window)进行序列化的实现,但是你可以提供你自己的实现,以便在以下任何组件中使用:

  • EncodingPayloadTransformer

  • DecodingTransformer

  • CodecMessageConverter

# EncodingPayloadTransformer

该转换器通过使用编解码器将有效负载编码为byte[]。它不影响消息头。

有关更多信息,请参见Javadoc (opens new window)

# DecodingTransformer

该转换器利用编解码器对byte[]进行解码。它需要配置一个Class来对对象进行解码(或者一个解析为Class的表达式)。如果生成的对象是Message<?>,则不保留入站标头。

有关更多信息,请参见Javadoc (opens new window)

# CodecMessageConverter

某些端点(例如 TCP 和 Redis)没有消息头的概念。它们支持使用MessageConverter,并且CodecMessageConverter可用于将消息转换为或从byte[]进行传输。

有关更多信息,请参见Javadoc (opens new window)

# 克里奥

目前,这是Codec的唯一实现,并且它提供了两种Codec:

  • PojoCodec:用于转换器

  • MessageCodec:用于CodecMessageConverter

该框架提供了几个自定义序列化器:

  • FileSerializer

  • MessageHeadersSerializer

  • MutableMessageHeadersSerializer

通过使用FileKryoRegistrarPojoCodec进行初始化,第一个可以与PojoCodec一起使用。第二个和第三个与MessageCodec一起使用,后者是用MessageKryoRegistrar初始化的。

# 定制 Kryo

默认情况下,Kryo 将未知的 Java 类型委托给它的FieldSerializer。Kryo 还为每个基元类型注册默认的序列化器,以及StringCollectionMapFieldSerializer使用反射来导航对象图。一种更有效的方法是实现一个自定义序列化器,它了解对象的结构,并可以直接序列化选定的原语字段。下面的示例展示了这样的序列化器:

public class AddressSerializer extends Serializer<Address> {

    @Override
    public void write(Kryo kryo, Output output, Address address) {
        output.writeString(address.getStreet());
        output.writeString(address.getCity());
        output.writeString(address.getCountry());
    }

    @Override
    public Address read(Kryo kryo, Input input, Class<Address> type) {
        return new Address(input.readString(), input.readString(), input.readString());
    }
}

Serializer接口公开KryoInputOutput,它们提供对包括哪些字段和其他内部设置的完全控制,如Kryo 文档 (opens new window)中所述。

在注册自定义序列化器时,你需要一个注册 ID,
注册 ID 是任意的,
但是,在我们的情况下,必须显式地定义 ID,因为分布式应用程序中的每个 Kryo 实例必须使用相同的 ID。,
Kryo 建议使用小的正整数,并保留一些 ID(值 <10), Spring 集成目前默认使用 40,41,和 42(对于前面提到的文件和消息头序列化器)。
我们建议你从 60 开始,以允许在框架中进行扩展。
你可以通过配置前面提到的注册商来覆盖这些框架默认设置。
# 使用自定义 Kryo 序列化器

如果你需要定制序列化,请参阅Kryo (opens new window)文档,因为你需要使用本机 API 来进行定制。例如,参见[MessageCodec](https://github.com/ Spring-projects/ Spring-integration/blob/main/ Spring-integration-core/SRC/main/java/org/springframework/integration/codec/kryo/messagecodec.java)的实现。

# 实现 kryoserializable

如果具有对域对象源代码的写访问权限,则可以实现KryoSerializable所描述的here (opens new window)。在这种情况下,类本身提供序列化方法,不需要进一步的配置。然而,基准测试表明,这并不像显式注册自定义序列化器那样有效。下面的示例展示了一个自定义的 Kryo 序列化器:

public class Address implements KryoSerializable {
    ...

    @Override
    public void write(Kryo kryo, Output output) {
        output.writeString(this.street);
        output.writeString(this.city);
        output.writeString(this.country);
    }

    @Override
    public void read(Kryo kryo, Input input) {
        this.street = input.readString();
        this.city = input.readString();
        this.country = input.readString();
    }
}

你也可以使用此技术来包装 Kryo 以外的序列化库。

# 使用@DefaultSerializer注释

Kryo 还提供了@DefaultSerializer注释,如here (opens new window)所述。

@DefaultSerializer(SomeClassSerializer.class)
public class SomeClass {
       // ...
}

如果你对域对象具有写访问权限,这可能是指定自定义序列化器的一种更简单的方法。请注意,这不会用 ID 注册类,这可能会使该技术在某些情况下无用。