内容丰富器

有时,你可能需要用比目标系统提供的信息更多的数据来丰富请求。数据丰富器(data enricher)模式描述了各种场景以及让你能够满足此类需求的组件(Enricher)。

Spring Integration 的 Core 模块包含两个丰富器:

它还包含三个适配器特定的头部丰富器:

请参阅本参考手册中有关这些适配器的特定章节,以了解更多信息。

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

头部丰富器

如果你只需要为消息添加头部,并且这些头部不是由消息内容动态确定的,那么引用自定义的转换器实现可能有点大材小用。因此,Spring Integration 提供了对头部丰富器模式的支持。它通过 <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>

头部丰富器还提供了方便的子元素来设置一些众所周知的头部名称,如下例所示:

<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> 子元素来同时提供头部“name”和“value”。

从 4.1 版本开始,头部丰富器提供了 routing-slip 子元素。有关更多信息,请参阅 路由单

POJO 支持

通常,头部值不能静态定义,而必须根据消息中的某些内容动态确定。这就是为什么头部丰富器允许你使用 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 Integration 2.0 中,我们引入了 Spring Expression Language (SpEL) 的便利性,以帮助配置许多不同的组件。头部丰富器就是其中之一。再次查看前面所示的 POJO 示例。你可以看到确定头部值的计算逻辑非常简单。一个自然的问题是:“有没有更简单的方法来实现这一点?” 这就是 SpEL 展现其真正强大之处的地方。考虑以下示例:

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

通过在这些简单情况下使用 SpEL,你不再需要提供单独的类并在应用程序上下文中配置它。你只需使用有效的 SpEL 表达式配置 expression 属性即可。变量“payload”和“headers”绑定到 SpEL 评估上下文,让你完全访问传入的消息。

使用 Java 配置头部丰富器

以下两个示例展示了如何使用 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 配置头部丰富器

以下示例展示了使用 Java DSL 配置头部丰富器:

@Bean
public IntegrationFlow enrichHeadersInFlow() {
    return f -> f
                ...
                .enrichHeaders(h -> h.header("emailUrl", this.emailUrl)
                                     .headerExpression("from", "payload.from[0].toString()"))
                .handle(...);
}

头部通道注册表

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

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

从 4.1 版本开始,你可以在 <bean/> 定义上将一个名为 removeOnGet 的属性设置为 true,映射条目将在首次使用时立即移除。这在高并发环境以及通道只使用一次的情况下可能非常有用,无需等待 reaper 移除它。

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 版本开始,你现在可以覆盖注册表配置的 reaper 延迟,以便无论 reaper 延迟如何,通道映射都至少保留指定的时间。以下示例展示了如何执行此操作:

<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 运算符(?:)在没有头部时使用两分钟。

负载丰富器

在某些情况下,如前所述的头部丰富器可能不够,负载本身可能需要用额外的信息来丰富。例如,进入 Spring Integration 消息传递系统的订单消息必须根据提供的客户编号查找订单的客户,然后用该信息丰富原始负载。

Spring Integration 2.1 引入了负载丰富器。负载丰富器定义了一个端点,该端点将 Message 传递给暴露的请求通道,然后等待回复消息。回复消息随后成为评估表达式以丰富目标负载的根对象。

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

本质上,通过定义请求通道,负载丰富器充当网关,等待发送到请求通道的消息返回。然后,丰富器使用回复消息提供的数据来增强消息的负载。

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

负载的丰富是通过 SpEL 表达式配置的,提供了最大程度的灵活性。因此,你不仅可以使用回复通道的 Message 中的直接值来丰富负载,还可以使用 SpEL 表达式从该消息中提取子集或应用额外的内联转换,让你进一步操纵数据。

如果你只需要用静态值丰富负载,则无需提供 request-channel 属性。

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

配置

以下示例展示了负载丰富器的所有可用配置选项:

<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 生命周期属性,表示此组件是否应在应用程序上下文启动期间启动。默认为 true。可选。
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 如果 request-channel 下游发生 Exception,则将 ErrorMessage 发送到该通道。这使你可以返回一个替代对象用于丰富。如果未设置此项,则会向调用者抛出 Exception。可选。
9 向通道发送消息时等待的最大毫秒数,如果通道可能阻塞的话。例如,如果队列通道已达到最大容量,则可能阻塞直到有可用空间。在内部,send() 超时设置在 MessagingTemplate 上,并最终在调用 MessageChannel 上的发送操作时应用。默认情况下,send() 超时设置为“30”。可选。
10 布尔值,指示任何实现 Cloneable 的负载在发送到请求通道获取丰富数据之前是否应该被克隆。克隆版本将用作最终回复的目标负载。默认值为 false。可选。
11 如果此端点是轮询消费者,允许你配置消息轮询器。可选。
12 每个 property 子元素提供了一个属性的名称(通过必需的 name 属性)。该属性应可在目标负载实例上设置。valueexpression 属性中必须且只能提供一个——前者用于设置字面量值,后者用于评估 SpEL 表达式。评估上下文的根对象是从此丰富器发起的流返回的消息——如果没有请求通道,则是输入消息;或者是应用程序上下文(使用 @<beanName>.<beanProperty> SpEL 语法)。从 4.0 版本开始,指定 value 属性时,还可以指定可选的 type 属性。当目标是类型化 setter 方法时,框架会适当地强制转换该值(只要存在 PropertyEditor)来处理转换。但是,如果目标负载是一个 Map,则直接使用该值填充条目,而不进行转换。type 属性允许你例如将包含数字的 String 转换为目标负载中的 Integer 值。从 4.1 版本开始,你还可以指定可选的 null-result-expression 属性。当 enricher 返回 null 时,会评估此表达式,并返回评估结果作为替代。
13 每个 header 子元素提供了一个消息头部的名称(通过必需的 name 属性)。valueexpression 属性中也必须且只能提供一个——前者用于设置字面量值,后者用于评估 SpEL 表达式。评估上下文的根对象是从此丰富器发起的流返回的消息——如果没有请求通道,则是输入消息;或者是应用程序上下文(使用 '@<beanName>.<beanProperty>' SpEL 语法)。请注意,类似于 <header-enricher><enricher> 元素的 header 元素具有 typeoverwrite 属性。然而,一个关键区别是,对于 <enricher>overwrite 属性默认为 true,以与 <enricher> 元素的 <property> 子元素保持一致。从 4.1 版本开始,你还可以指定一个可选的 null-result-expression 属性。当 enricher 返回 null 时,会评估此表达式,并返回评估结果作为替代。

示例

本节包含在各种情况下使用负载丰富器的一些示例。

这里展示的代码示例是 Spring Integration Samples 项目的一部分。请参阅 Spring Integration 示例

在以下示例中,一个 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。丰富器的 request-channel 属性被配置为将 User 传递给 findUserServiceChannel

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

如何只将数据的一个子集传递给请求通道?

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

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

请记住,尽管只传递了 username,但发送到请求通道的最终消息仍然包含完整的 MessageHeaders

如何丰富包含集合数据的负载?

在以下示例中,传递的是一个 Map 对象,而不是一个 User 对象

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

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

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

以下示例完全不使用请求通道,而仅仅用静态值丰富消息的有效载荷

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

请注意,这里的“静态”一词用法比较灵活。您仍然可以使用 SpEL 表达式来设置这些值。