# 消息转换
## 消息转换
### 转换器
消息转换器在实现消息生产者和消息消费者之间的松耦合方面发挥着非常重要的作用。你可以在这些组件之间添加转换器,而不是要求每个产生消息的组件都知道下一个使用者期望的类型。通用的转换器,例如将`String`转换为 XML 文档的转换器,也是高度可重用的。
对于某些系统,最好是提供[规范数据模型](https://www.enterpriseintegrationpatterns.com/CanonicalDataModel.html),但是 Spring 集成的一般原理是不需要任何特定的格式。相反,为了获得最大的灵活性, Spring 集成旨在为扩展提供尽可能简单的模型。与其他端点类型一样,在 XML 或 Java 注释中使用声明式配置使简单的 POJO 能够适应消息转换器的角色。本章的其余部分将描述这些配置选项。
| |为了最大限度地提高灵活性, Spring 不需要基于 XML 的消息有效负载。
不过,如果这确实是你的应用程序的正确选择,那么该框架确实为处理基于 XML 的有效负载提供了一些方便的转换器。
有关这些转换器的更多信息,请参见[XML 支持-处理 XML 有效负载](./xml.html#xml)。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
#### 使用 XML 配置转换器
``元素用于创建消息转换端点。除了`input-channel`和`output-channel`属性外,它还需要一个`ref`属性。`ref`可以指向单个方法上包含`@Transformer`注释的对象(参见[用注解配置转换器](#transformer-annotation)),也可以与`method`属性中提供的显式方法名值结合。
```
```
如果定制的 Transformer 处理程序实现可以在其他``定义中重用,则通常建议使用`ref`属性。但是,如果自定义 Transformer 处理程序实现的范围应为``的单个定义,则可以定义内部 Bean 定义,如下例所示:
```
```
| |不允许在同一个``配置中同时使用`ref`属性和内部处理程序定义,因为这会创建一个模棱两可的条件,并导致抛出异常。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| |如果`ref`属性引用扩展`AbstractMessageProducingHandler`的 Bean(例如框架本身提供的转换器),则通过将输出通道直接注入处理程序来优化配置。,在这种情况下,
,每个`ref`必须是一个单独的 Bean 实例(或者`prototype`-作用域 Bean),或者使用内部的``配置类型。
如果你不小心从多个 bean 引用了相同的消息处理程序,则会出现配置异常。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
当使用 POJO 时,用于转换的方法可能期望入站消息的`Message`类型或有效负载类型。它还可以通过分别使用`@Header`和`@Headers`参数注释来单独或作为完整映射接受消息头值。方法的返回值可以是任何类型。如果返回值本身是`Message`,则将其传递到转换器的输出通道。
在 Spring Integration2.0 中,消息转换器的转换方法不再能够返回`null`。返回`null`将导致一个异常,因为消息转换器应该始终被期望将每个源消息转换为有效的目标消息。换句话说,消息转换器不应该被用作消息过滤器,因为它有一个专用的``选项。但是,如果你确实需要这种类型的行为(其中组件可能会返回`null`,这不应被视为错误),则可以使用服务激活器。其`requires-reply`值默认为`false`,但可以设置为`true`,以便为`null`抛出异常返回值,就像转换器一样。
#### 转换器与 Spring 表达式语言
与路由器、聚合器和其他组件一样,在 Spring Integration2.0 中,只要转换逻辑相对简单,转换器也可以从[SPEL 支持](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions)中受益。下面的示例展示了如何使用 SPEL 表达式:
```
```
前面的示例在不编写自定义转换器的情况下转换有效负载。我们的有效负载(假定为`String`)是上层的,与当前的时间戳连接在一起,并应用了一些格式。
#### 通用转换器
Spring 集成提供了一些转换器实现方式。
##### 对象到字符串转换器
因为使用`toString()`表示`Object`是相当常见的, Spring 集成提供了一个`ObjectToStringTransformer`,其输出是带有字符串`payload`的`Message`。这个`String`是在入站消息的有效负载上调用`toString()`操作的结果。下面的示例展示了如何声明对象到字符串转换器的实例:
```
```
此转换器的一个潜在用途是向`file`命名空间中的“出站通道适配器”发送一些任意对象。默认情况下,该通道适配器仅支持`String`、字节数组或`java.io.File`有效负载,在适配器处理必要的转换之前立即添加此转换器。只要`toString()`调用的结果是你想要写入到文件中的内容,那么它就可以正常工作。否则,你可以使用前面显示的通用“Transformer”元素来提供基于 POJO 的自定义转换器。
| |调试时,此转换器通常不是必需的,因为`logging-channel-adapter`能够记录消息有效负载。
有关更多详细信息,请参见[Wire Tap](./channel.html#channel-wiretap)。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| |Object-to-String Transformer 非常简单。它在入站有效负载上调用。自 Spring Integration3.0 以来,该规则有两个例外:* 如果有效负载是,则调用。如果有效负载是,它调用`new String(payload, charset)`,其中`charset`是默认的 UTF-8。
`charset`可以通过在转换器上提供字符集属性来修改
以获得更复杂的操作(例如在运行时动态地选择字符集),你可以使用基于 spel 表达式的转换器,如下例所示:
```
expression="new java.lang.String(payload, headers['myCharset']" />
```|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
如果需要将`Object`序列化为字节数组,或者将字节数组反序列化为`Object`, Spring 集成提供了对称的序列化转换器。这些默认情况下使用标准的 Java 序列化,但是可以通过分别使用`serializer`和`deserializer`属性提供 Spring `Serializer`或`Deserializer`策略的实现。下面的示例展示了如何使用 Spring 的序列化器和反序列化器:
```
```
| |当反序列化来自不受信任的源的数据时,你应该考虑添加一个`allow-list`的包和类模式。
默认情况下,所有类都是反序列化的。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
##### `Object`-to-`Map`和`Map`-to-`Object`转换器
Spring 集成还提供了`Object`-to-`Map`和`Map`-to-`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 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 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 nickNames;
// setters and getters are omitted
}
```
前面示例中的两个类被转换为以下`Map`:
```
{name=George, child={name=Jenna, nickNames=[Bimbo, ...]}}
```
Spring 为了配置这些转换器,Integration 为对象到映射提供了名称空间支持,如下例所示:
```
```
还可以将`flatten`属性设置为 false,如下所示:
```
```
Spring 集成为映射到对象提供了名称空间支持,如下例所示:
```
```
另外,可以使用`ref`属性和原型范围 Bean,如下例所示:
```
```
| |“ref”和“type”属性是互斥的。
此外,如果使用“ref”属性,则必须指向 Bean 范围内的“prototype”。
否则,将抛出一个`BeanCreationException`。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
从版本 5.0 开始,你可以为`ObjectToMapTransformer`提供自定义的`JsonObjectMapper`——当你需要为空集合的日期或空集合(以及其他用途)提供特殊格式时。有关`JsonObjectMapper`实现的更多信息,请参见[JSON 转换器](#json-transformers)。
##### 流转换器
`StreamTransformer`将`InputStream`有效载荷转换为`byte[]`(如果提供了`charset`,则转换为<`String`)。
下面的示例展示了如何在 XML 中使用`stream-transformer`元素:
```
```
下面的示例展示了如何使用`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 声明它们:
```
```
```
```
默认情况下,前一个列表中的 Transformers 使用的是 vanilla`JsonObjectMapper`。它是基于来自 Classpath 的一种实现方式。你可以为自己的自定义`JsonObjectMapper`实现提供适当的选项,或者基于所需的库(例如 GSON),如下例所示:
```
```
| |从版本 3.0 开始,`object-mapper`属性引用了一个新策略接口的实例:`JsonObjectMapper`。
此抽象允许使用 JSON 映射程序的多个实现。
提供了 wraps[Jackson 2](https://github.com/FasterXML)的实现,并在 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 中执行相同的操作。
```
```
| |从版本 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 还提供了一个用于表达式的内置`#jsonPath`spel 函数。有关更多信息,请参见[Spring Expression Language (SpEL)](./spel.html#spel)。
自版本 3.0 以来, Spring 集成还提供了用于表达式的内置`#xpath`spel 函数。有关更多信息,请参见[#XPath Spel 函数](./xml.html#xpath-spel-function)。
从版本 4.0 开始,`ObjectToJsonTransformer`支持`resultType`属性,以指定节点 JSON 表示。结果节点树表示依赖于所提供的`JsonObjectMapper`的实现。默认情况下,`ObjectToJsonTransformer`使用`Jackson2JsonObjectMapper`并将对象到节点树的转换委托给`ObjectMapper#valueToTree`方法。当下游消息流使用具有访问 JSON 数据属性的 SPEL 表达式时,节点 JSON 表示提供了使用`JsonPropertyAccessor`的效率。有关更多信息,请参见[属性访问器](./spel.html#spel-property-accessors)。
从版本 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`building 抛出一个`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`注释将`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);
}
```
另见[使用注释为端点提供建议](./handler-advice.html#advising-with-annotations)。
#### 页眉过滤器
有时,你的转换用例可能很简单,只需删除几个标题即可。对于这样的用例, Spring Integration 提供了一个头过滤器,它允许你指定应该从输出消息中删除的某些头名称(例如,出于安全原因删除头,或者仅暂时需要的值)。基本上,header 过滤器与 header enricher 相反。后者在[页眉 Enricher](./content-enrichment.html#header-enricher)中讨论。下面的示例定义了一个头过滤器:
```
```
正如你所看到的,头过滤器的配置非常简单。它是一个典型的端点,具有输入和输出通道以及`header-names`属性。该属性接受需要删除的标头的名称(如果有多个标头,则用逗号分隔)。因此,在前面的示例中,出站消息中不存在名为“lastname”和“state”的头。
#### 基于编解码器的转换器
见[Codec](./codec.html#codec)。
### 内容更丰富
有时,你可能需要使用比目标系统提供的更多信息来增强请求。[数据收集器](https://www.enterpriseintegrationpatterns.com/DataEnricher.html)模式描述了各种场景以及允许你满足此类需求的组件。
Spring Integration`Core`模块包括两个增强器:
* [页眉 Enricher](#header-enricher)
* [有效载荷 Enricher](#payload-enricher)
它还包括三个特定于适配器的头增强程序:
* [XPath Header Enricher(XML 模块)](./xml.html#xml-xpath-header-enricher)
* [Mail Header Enricher(邮件模块)](./mail.html#mail-namespace)
* [XMPP Header Enricher(XMPP 模块)](./xmpp.html#xmpp-message-outbound-channel-adapter)
有关这些适配器的更多信息,请参阅本参考手册中与适配器相关的部分。
有关表达式支持的更多信息,请参见[Spring Expression Language (SpEL)](./spel.html#spel)。
#### 页眉 Enricher
如果只需要向消息添加头,而头不是由消息内容动态确定的,那么引用 Transformer 的自定义实现可能会导致过度使用。出于这个原因, Spring 集成为 Header Enricher 模式提供了支持。它是通过``元素公开的。下面的示例展示了如何使用它:
```
```
Header Enricher 还提供了有用的子元素来设置众所周知的头名称,如下例所示:
```
```
前面的配置显示,对于已知的头(例如`errorChannel`,`correlationId`,`priority`,`replyChannel`,`routing-slip`,以及其他),而不是使用泛型``子元素,其中必须同时提供头’名称’和’值’,你可以使用方便的子元素直接设置这些值。
从版本 4.1 开始,Header Enricher 提供了一个`routing-slip`子元素。有关更多信息,请参见[布线滑移](./router.html#routing-slip)。
##### POJO 支持
通常,头文件的值不能静态地定义,而必须根据消息中的某些内容动态地确定。这就是为什么 Header Enricher 允许你通过使用`ref`和`method`属性来指定 Bean 引用的原因。指定的方法计算标头值。考虑以下配置和具有修改`String`的方法的 Bean:
```
```
```
public class MyBean {
public String computeValue(String payload){
return payload.toUpperCase() + "_US";
}
}
```
还可以将 POJO 配置为内部 Bean,如下例所示:
```
```
你可以类似地指向一个 Groovy 脚本,如下例所示:
```
```
##### spel 支持
在 Spring Integration2.0 中,我们引入了[Spring Expression Language (SpEL)](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#expressions)的便利,以帮助配置许多不同的组件。标题 Enricher 就是其中之一。再看看前面显示的 POJO 示例。你可以看到,确定标头值的计算逻辑非常简单。一个自然的问题是:“有没有更简单的方法来实现这一点?”这就是 SPEL 展现其真正力量的地方。考虑以下示例:
```
```
通过在这种简单的情况下使用 SPEL,你不再需要提供一个单独的类并在应用程序上下文中对其进行配置。你所需要做的就是将`expression`属性配置为一个有效的 SPEL 表达式。“payload”和“headers”变量绑定到 SPEL 评估上下文,从而使你能够完全访问传入的消息。
##### 使用 Java 配置来配置标题 Enricher
下面的两个示例展示了如何使用 Java 配置来增强报头:
```
@Bean
@Transformer(inputChannel = "enrichHeadersChannel", outputChannel = "emailChannel")
public HeaderEnricher enrichHeaders() {
Map> 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> headersToAdd = new HashMap<>();
headersToAdd.put("emailUrl", new StaticHeaderValueMessageProcessor(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 开始,一个新的子元素``可用。它没有任何属性。这个新的子元素将现有的`replyChannel`和`errorChannel`头(当它们是`MessageChannel`时)转换为`String`,并将通道存储在注册表中,以便以后在需要发送回复或处理错误时进行解析。这对于可能丢失消息头的情况很有用——例如,将消息序列化到消息存储中或通过 JMS 传输消息时。如果标题不存在,或者它不是`MessageChannel`,则不进行任何更改。
使用此功能需要存在`HeaderChannelRegistry` Bean。默认情况下,该框架创建一个`DefaultHeaderChannelRegistry`,默认过期时间为 60 秒。在此时间之后,通道将从注册表中删除。要更改此行为,请使用`id`的`integrationHeaderChannelRegistry`定义 Bean,并使用构造函数参数(以毫秒为单位)配置所需的默认延迟。
从版本 4.1 开始,你可以在``定义上将一个名为`removeOnGet`的属性设置为`true`,并且在第一次使用时立即删除该映射条目。这在大容量环境中以及通道只被使用一次(而不是等待收割者删除它)时可能是有用的。
`HeaderChannelRegistry`有一个`size()`方法来确定注册表的当前大小。`runReaper()`方法取消当前的计划任务并立即运行 Reaper。然后根据当前的延迟将任务安排为再次运行。可以通过获取对注册表的引用来直接调用这些方法,或者可以向控制总线发送带有以下内容的消息:
```
"@integrationHeaderChannelRegistry.runReaper()"
```
这个子元素很方便,相当于指定了以下配置:
```
```
从版本 4.1 开始,你现在可以重写注册表配置的收割者延迟,以便至少在指定的时间内保留通道映射,而不考虑收割者延迟。下面的示例展示了如何做到这一点:
```
```
在第一种情况下,每个头通道映射的可用时间为两分钟。在第二种情况下,在消息头中指定了生存时间,如果没有消息头,则使用 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 的所有可用配置选项:
```
(10)
(11)
(12)
(13)
```
|**1** |发送消息以获取用于充实的数据的通道。
可选的。|
|------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2** |生命周期属性表示是否应该在应用程序上下文启动期间启动此组件。
默认为真。
可选。|
|**3** |底层 Bean 定义的 ID,它是`EventDrivenConsumer`或`PollingConsumer`的可选项。|
|**4** |指定当此端点作为订阅服务器连接到某个通道时的调用顺序。
当该通道使用“故障转移”调度策略时,这一点尤其相关。
当该端点本身是具有队列的通道的轮询消费者时,它没有任何作用。
可选的。|
|**5** |标识在此端点处理消息后发送消息的消息通道。
可选的。|
|**6** |默认情况下,原始消息的有效负载被用作发送到`request-channel`的有效负载,
通过指定一个 SPEL 表达式作为`request-payload-expression`属性的值,你可以使用原始有效负载的一个子集,一个标头值,或任何其他可解析的 SPEL 表达式作为发送到请求通道的有效负载的基础。
对于表达式求值,完整的消息作为“根对象”可用。
例如,以下 SPEL 表达式(其中包括)是可能的:`payload.something`,`headers.something`,`new 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`属性)。
该属性应该可以在目标有效负载实例上设置。
正好是`value`或`expression`属性中的一个属性也必须提供——前一个属性对于要设置的文字值和要计算的 SPEL 表达式的文字值。
计算上下文的根对象是从这个 Enricher 启动的流返回的消息——输入消息如果没有请求通道或应用程序上下文(使用`@.`spel 语法)。
从版本 4.0 开始,当指定`value`属性时,还可以指定可选的`type`属性。
当目标是类型化的 setter 方法时,框架会适当地强制该值(只要存在`PropertyEditor`)来处理转换。
但是,如果目标负载是`Map`,则该条目将填充该值而不进行转换。
例如,`type`属性允许你这样做,将包含数字的`String`转换为目标有效载荷中的`Integer`值。
从版本 4.1 开始,你还可以指定一个可选的`null-result-expression`属性。
当`enricher`返回 null 时,将对其进行求值,并返回求值的输出。|
|**13**|每个`header`子元素都提供了消息头的名称(通过强制`name`属性)。
正好是`value`或`expression`属性中的一个,也必须提供前者以设置文本值计算上下文的根对象是从这个 Enricher 发起的流返回的消息——输入消息如果没有请求通道或应用程序上下文(使用’@\.\’spel 语法)。
请注意,与``类似,``元素的`header`元素具有`type`和`overwrite`属性。但是,与
属性不同的是,默认情况下,`overwrite`属性是`true`,以与``元素的``子元素保持一致。
从版本 4.1 开始,你还可以指定一个可选的`null-result-expression`属性。
当`enricher`返回 null 时,将对其进行求值,并返回评估的输出。|
##### 示例
本节包含在各种情况下使用有效负载 Enricher 的几个示例。
| |这里显示的代码示例是 Spring 集成示例项目的一部分。
参见[Spring Integration Samples](./samples.html#samples)。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------|
在下面的示例中,一个`User`对象作为`Message`的有效负载传递:
```
```
`User`有几个属性,但最初只设置`username`。Enricher 的`request-channel`属性被配置为将`User`传递给`findUserServiceChannel`。
通过隐式设置`reply-channel`,返回一个`User`对象,并通过使用`property`子元素,从应答中提取属性并用于丰富原始有效负载。
##### 如何仅将数据的子集传递给请求通道?
当使用`request-payload-expression`属性时,可以将有效负载的单个属性而不是完整消息传递到请求通道。在下面的示例中,用户名属性被传递到请求通道:
```
```
请记住,尽管只传递了用户名,但发送到请求通道的结果消息包含完整的`MessageHeaders`集。
###### 如何充实由收集数据组成的有效载荷?
在下面的示例中,传入的不是`User`对象,而是`Map`对象:
```
```
`Map`包含`username`Map 键下的用户名。只有`username`被传递到请求通道。该回复包含一个完整的`User`对象,该对象最终被添加到`Map`键下的`user`中。
##### 在不使用请求通道的情况下,如何使用静态信息丰富有效负载?
下面的示例根本不使用请求通道,而只使用静态值来丰富消息的有效负载:
```
```
请注意,“static”一词在这里的用法并不严谨。你仍然可以使用 SPEL 表达式来设置这些值。
### 索赔检查
在前面的部分中,我们介绍了几个内容更丰富的组件,它们可以帮助你处理消息丢失一段数据的情况。我们还讨论了内容过滤,它允许你从消息中删除数据项。然而,有时我们希望暂时隐藏数据。例如,在分布式系统中,我们可能会接收到具有非常大的有效负载的消息。一些间歇性的消息处理步骤可能不需要访问此有效负载,而一些可能只需要访问某些标头,因此通过每个处理步骤携带较大的消息有效负载可能会导致性能下降,可能会产生安全风险,并可能使调试更加困难。
[存储在图书馆中](https://www.enterpriseintegrationpatterns.com/StoreInLibrary.html)(或 ClaimCheck)模式描述了一种机制,该机制允许你将数据存储在一个众所周知的位置,同时仅维护指向该数据所在位置的指针(ClaimCheck)。你可以将该指针作为新消息的有效负载传递,从而让消息流中的任何组件在需要时立即获得实际数据。这种方法非常类似于认证邮件流程,即你在邮箱中收到一张索赔支票,然后必须去邮局领取你的实际包裹。这也是同样的想法后,在飞机上或在酒店提取行李。
Spring 集成提供了两种类型的索赔检查转换器:
* 进站索赔检查转换器
* 出口索赔检查转换器
可以使用方便的基于名称空间的机制来配置它们。
#### 进件索赔检查转换器
传入的索赔检查转换器通过将传入的消息存储在其`message-store`属性标识的消息存储区中来转换该消息。以下示例定义了传入的索赔检查转换器:
```
```
在前面的配置中,在`input-channel`上接收的消息被持久化到用`message-store`属性标识的消息存储中,并用生成的 ID 进行索引。该 ID 是该消息的索赔检查。索赔检查也成为发送到`output-channel`的新(转换)消息的有效负载。
现在,假设在某个时刻你确实需要访问实际的消息。你可以手动访问消息存储区并获取消息的内容,或者可以使用相同的方法(创建一个转换器),但现在你可以使用一个传出的 ClaimCheck 转换器将 ClaimCheck 转换为实际的消息。
下面的清单提供了传入的索赔检查转换器的所有可用参数的概述:
```
(7)
(8)
```
|**1**|生命周期属性表示是否应该在应用程序上下文启动期间启动此组件。
它默认为`true`。
此属性在`Chain`元素内不可用。
可选。|
|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|标识基础 Bean 定义(`MessageTransformingHandler`)。
该属性在`Chain`元素中不可用。
可选。|
|**3**|此端点的接收消息通道。
此属性在`Chain`元素内不可用。
可选。|
|**4**|引用`MessageStore`将由此索赔检查转换器使用。
如果未指定,默认引用是对名为`messageStore`的 Bean 的引用。
可选的。|
|**5**|指定当此端点作为订阅服务器连接到一个通道时调用的顺序。
这在以下情况下特别相关该通道使用`failover`调度策略。
当该端点本身是具有队列的通道的轮询消费者时,它没有任何作用。
该属性在`Chain`元素中不可用。
可选。|
|**6**|标识由该端点处理后发送消息的消息通道。
该属性在`Chain`元素中不可用。
可选。|
|**7**|指定向输出通道发送回复消息时等待的最长时间(以毫秒为单位)。
默认为`-1`—无限期地阻塞。
此属性在`Chain`元素内不可用。
可选。|
|**8**|定义一个 poller。
这个元素在`Chain`元素中是不可用的。
可选的。|
#### 出口索赔检查转换器
一个传出的索赔检查转换器允许你将具有索赔检查有效负载的消息转换为具有原始内容作为其有效负载的消息。
```
```
在前面的配置中,在`input-channel`上收到的消息应该有一个索赔检查作为其有效负载。出站索赔检查转换器通过在消息存储中查询由提供的索赔检查标识的消息,将其转换为带有原始有效负载的消息。然后,它将新签出的消息发送到`output-channel`。
下面的清单提供了即将发出的索赔检查转换器的所有可用参数的概述:
```
(8)
(9)
```
|**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-out`Transformer 上引入了`remove-message`布尔属性。默认情况下,此属性设置为`false`。但是,如果设置为`true`,则从`MessageStore`中删除已声明的消息,因此不能再次声明该消息。
此功能在存储空间方面具有影响,特别是在基于内存`Map`的`SimpleMessageStore`的情况下,在这种情况下,无法删除消息可能最终导致`OutOfMemoryException`。因此,如果你不希望提出多个索赔,我们建议你将`remove-message`属性的值设置为`true`。下面的示例展示了如何使用`remove-message`属性:
```
```
#### 消息存储中的一个单词
尽管我们很少关心索赔检查的细节(只要它们有效),但你应该知道 Spring 集成中实际索赔检查(指针)的当前实现使用 UUID 来确保唯一性。
`org.springframework.integration.store.MessageStore`是用于存储和检索消息的策略接口。 Spring 集成提供了 IT 的两种方便的实现方式:
* `SimpleMessageStore`:一个基于内存的`Map`实现(缺省的,适合测试)
* `JdbcMessageStore`:在 JDBC 上使用关系数据库的实现
### 编解码器
Spring 集成的 4.2 版本引入了`Codec`抽象。编解码器对`byte[]`之间的对象进行编码和解码。它们为 Java 序列化提供了一种替代方案。一个优点是,通常,对象不需要实现`Serializable`。我们提供了一个使用[Kryo](https://github.com/EsotericSoftware/kryo)进行序列化的实现,但是你可以提供你自己的实现,以便在以下任何组件中使用:
* `EncodingPayloadTransformer`
* `DecodingTransformer`
* `CodecMessageConverter`
#### `EncodingPayloadTransformer`
该转换器通过使用编解码器将有效负载编码为`byte[]`。它不影响消息头。
有关更多信息,请参见[Javadoc](https://docs.spring.io/spring-integration/api/org/springframework/integration/transformer/EncodingPayloadTransformer.html)。
#### `DecodingTransformer`
该转换器利用编解码器对`byte[]`进行解码。它需要配置一个`Class`来对对象进行解码(或者一个解析为`Class`的表达式)。如果生成的对象是`Message>`,则不保留入站标头。
有关更多信息,请参见[Javadoc](https://docs.spring.io/spring-integration/api/org/springframework/integration/transformer/DecodingTransformer.html)。
#### `CodecMessageConverter`
某些端点(例如 TCP 和 Redis)没有消息头的概念。它们支持使用`MessageConverter`,并且`CodecMessageConverter`可用于将消息转换为或从`byte[]`进行传输。
有关更多信息,请参见[Javadoc](https://docs.spring.io/spring-integration/api/org/springframework/integration/codec/CodecMessageConverter.html)。
#### 克里奥
目前,这是`Codec`的唯一实现,并且它提供了两种`Codec`:
* `PojoCodec`:用于转换器
* `MessageCodec`:用于`CodecMessageConverter`
该框架提供了几个自定义序列化器:
* `FileSerializer`
* `MessageHeadersSerializer`
* `MutableMessageHeadersSerializer`
通过使用`FileKryoRegistrar`对`PojoCodec`进行初始化,第一个可以与`PojoCodec`一起使用。第二个和第三个与`MessageCodec`一起使用,后者是用`MessageKryoRegistrar`初始化的。
##### 定制 Kryo
默认情况下,Kryo 将未知的 Java 类型委托给它的`FieldSerializer`。Kryo 还为每个基元类型注册默认的序列化器,以及`String`、`Collection`和`Map`。`FieldSerializer`使用反射来导航对象图。一种更有效的方法是实现一个自定义序列化器,它了解对象的结构,并可以直接序列化选定的原语字段。下面的示例展示了这样的序列化器:
```
public class AddressSerializer extends Serializer {
@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 type) {
return new Address(input.readString(), input.readString(), input.readString());
}
}
```
`Serializer`接口公开`Kryo`、`Input`和`Output`,它们提供对包括哪些字段和其他内部设置的完全控制,如[Kryo 文档](https://github.com/EsotericSoftware/kryo)中所述。
| |在注册自定义序列化器时,你需要一个注册 ID,
注册 ID 是任意的,
但是,在我们的情况下,必须显式地定义 ID,因为分布式应用程序中的每个 Kryo 实例必须使用相同的 ID。,
Kryo 建议使用小的正整数,并保留一些 ID(值 \<10), Spring 集成目前默认使用 40,41,和 42(对于前面提到的文件和消息头序列化器)。
我们建议你从 60 开始,以允许在框架中进行扩展。
你可以通过配置前面提到的注册商来覆盖这些框架默认设置。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
###### 使用自定义 Kryo 序列化器
如果你需要定制序列化,请参阅[Kryo](https://github.com/EsotericSoftware/kryo)文档,因为你需要使用本机 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](https://github.com/EsotericSoftware/kryo#kryoserializable)。在这种情况下,类本身提供序列化方法,不需要进一步的配置。然而,基准测试表明,这并不像显式注册自定义序列化器那样有效。下面的示例展示了一个自定义的 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](https://github.com/EsotericSoftware/kryo#default-serializers)所述。
```
@DefaultSerializer(SomeClassSerializer.class)
public class SomeClass {
// ...
}
```
如果你对域对象具有写访问权限,这可能是指定自定义序列化器的一种更简单的方法。请注意,这不会用 ID 注册类,这可能会使该技术在某些情况下无用。