消息转换器

AmqpTemplate 还定义了几个用于发送和接收消息的方法,这些方法都委托给 MessageConverterMessageConverter 为每个方向提供了一个方法:一个用于将对象转换 Message,另一个用于将对象从 Message 转换。请注意,在转换为 Message 时,除了对象之外,您还可以提供属性。object 参数通常对应于 Message 正文。以下列表显示了 MessageConverter 接口定义

public interface MessageConverter {

    Message toMessage(Object object, MessageProperties messageProperties)
            throws MessageConversionException;

    Object fromMessage(Message message) throws MessageConversionException;

}

AmqpTemplate 上相关的 Message 发送方法比我们之前讨论的方法更简单,因为它们不需要 Message 实例。相反,MessageConverter 负责通过将提供的对象转换为 Message 正文的字节数组,然后添加任何提供的 MessageProperties 来“创建”每个 Message。以下列表显示了各种方法的定义

void convertAndSend(Object message) throws AmqpException;

void convertAndSend(String routingKey, Object message) throws AmqpException;

void convertAndSend(String exchange, String routingKey, Object message)
    throws AmqpException;

void convertAndSend(Object message, MessagePostProcessor messagePostProcessor)
    throws AmqpException;

void convertAndSend(String routingKey, Object message,
    MessagePostProcessor messagePostProcessor) throws AmqpException;

void convertAndSend(String exchange, String routingKey, Object message,
    MessagePostProcessor messagePostProcessor) throws AmqpException;

在接收端,只有两个方法:一个接受队列名称,另一个依赖于模板的“队列”属性已设置。以下列表显示了这两个方法的定义

Object receiveAndConvert() throws AmqpException;

Object receiveAndConvert(String queueName) throws AmqpException;
异步消费者 中提到的 MessageListenerAdapter 也使用 MessageConverter

SimpleMessageConverter

MessageConverter 策略的默认实现称为 SimpleMessageConverter。如果您没有明确配置替代方案,RabbitTemplate 实例会使用此转换器。它处理基于文本的内容、序列化的 Java 对象和字节数组。

Message 转换

如果输入 Message 的内容类型以“text”开头(例如,“text/plain”),它还会检查 content-encoding 属性,以确定将 Message 正文字节数组转换为 Java String 时要使用的字符集。如果输入 Message 上未设置 content-encoding 属性,则默认使用 UTF-8 字符集。如果您需要覆盖该默认设置,您可以配置一个 SimpleMessageConverter 实例,设置其 defaultCharset 属性,并将其注入 RabbitTemplate 实例。

如果输入 Message 的 content-type 属性值设置为“application/x-java-serialized-object”,则 SimpleMessageConverter 尝试将字节数组反序列化(重新水化)为 Java 对象。虽然这对于简单的原型设计可能有用,但我们不建议依赖 Java 序列化,因为它会导致生产者和消费者之间的紧密耦合。当然,它也排除了在任一侧使用非 Java 系统。AMQP 是一个线级协议,如果因为这些限制而失去大部分优势,那将是不幸的。在接下来的两节中,我们将探讨一些在不依赖 Java 序列化的情况下传递富领域对象内容的替代方案。

对于所有其他内容类型,SimpleMessageConverter 直接将 Message 正文内容作为字节数组返回。

有关重要信息,请参阅 Java 反序列化

转换为 Message

当将任意 Java 对象转换为 Message 时,SimpleMessageConverter 同样处理字节数组、字符串和可序列化实例。它将这些对象转换为字节(对于字节数组,无需转换),并相应地设置 content-type 属性。如果要转换的 Object 不属于这些类型之一,则 Message 正文为 null。

SerializerMessageConverter

此转换器类似于 SimpleMessageConverter,不同之处在于它可以配置其他 Spring Framework SerializerDeserializer 实现,用于 application/x-java-serialized-object 转换。

有关重要信息,请参阅 Java 反序列化

JacksonJsonMessageConverter

本节介绍如何使用 JacksonJsonMessageConverter 转换为和从 Message。它包含以下几个部分

AbstractJackson2MessageConverter、其实现和相关的 Jackson2JavaTypeMapper API 已在 4.0 版本中弃用,并计划移除,取而代之的是基于 Jackson 3 的相应类。有关相应的迁移指南,请参阅已弃用类的 JavaDocs。

转换为 Message

如前一节所述,通常不建议依赖 Java 序列化。一种更灵活、更易于跨不同语言和平台移植的常见替代方案是 JSON(JavaScript 对象表示法)。该转换器可以在任何 RabbitTemplate 实例上配置,以覆盖其对 SimpleMessageConverter 默认值的使用。JacksonJsonMessageConverter 使用 Jackson 3.x 库。以下示例配置了一个 JacksonJsonMessageConverter

<bean class="org.springframework.amqp.rabbit.core.RabbitTemplate">
    <property name="connectionFactory" ref="rabbitConnectionFactory"/>
    <property name="messageConverter">
        <bean class="org.springframework.amqp.support.converter.JacksonJsonMessageConverter">
            <!-- if necessary, override the DefaultClassMapper -->
            <property name="classMapper" ref="customClassMapper"/>
        </bean>
    </property>
</bean>

如上所示,JacksonJsonMessageConverter 默认使用 DefaultClassMapper。类型信息被添加到(并从)MessageProperties 中检索。如果入站消息在 MessageProperties 中不包含类型信息,但您知道预期类型,则可以使用 defaultType 属性配置静态类型,如下例所示

<bean id="jsonConverterWithDefaultType"
      class="o.s.amqp.support.converter.JacksonJsonMessageConverter">
    <property name="classMapper">
        <bean class="org.springframework.amqp.support.converter.DefaultClassMapper">
            <property name="defaultType" value="thing1.PurchaseOrder"/>
        </bean>
    </property>
</bean>

此外,您可以提供从 TypeId 头部的值到自定义映射。以下示例展示了如何实现

@Bean
public JacksonJsonMessageConverter jsonMessageConverter() {
    JacksonJsonMessageConverter jsonConverter = new JacksonJsonMessageConverter();
    jsonConverter.setClassMapper(classMapper());
    return jsonConverter;
}

@Bean
public DefaultClassMapper classMapper() {
    DefaultClassMapper classMapper = new DefaultClassMapper();
    Map<String, Class<?>> idClassMapping = new HashMap<>();
    idClassMapping.put("thing1", Thing1.class);
    idClassMapping.put("thing2", Thing2.class);
    classMapper.setIdClassMapping(idClassMapping);
    return classMapper;
}

现在,如果发送系统将头部设置为 thing1,转换器将创建 Thing1 对象,依此类推。有关从非 Spring 应用程序转换消息的完整讨论,请参阅 从非 Spring 应用程序接收 JSON 示例应用程序。

从版本 2.4.3 开始,如果 supportedMediaType 具有 charset 参数,转换器将不再添加 contentEncoding 消息属性;这也用于编码。已添加新方法 setSupportedMediaType

String utf16 = "application/json; charset=utf-16";
converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16));

Message 转换

根据发送系统添加到头部中的类型信息,入站消息被转换为对象。

从版本 2.4.3 开始,如果不存在 contentEncoding 消息属性,转换器将尝试检测 contentType 消息属性中的 charset 参数并使用它。如果两者都不存在,如果 supportedMediaType 具有 charset 参数,它将用于解码,最终回退到 defaultCharset 属性。已添加新方法 setSupportedMediaType

String utf16 = "application/json; charset=utf-16";
converter.setSupportedContentType(MimeTypeUtils.parseMimeType(utf16));

在 1.6 之前的版本中,如果不存在类型信息,转换将失败。从 1.6 版本开始,如果缺少类型信息,转换器将使用 Jackson 默认值(通常是 map)转换 JSON。

此外,从 1.6 版本开始,当您使用 @RabbitListener 注解(在方法上)时,推断的类型信息会添加到 MessageProperties 中。这允许转换器转换为目标方法的参数类型。这仅适用于只有一个没有注解的参数或一个带有 @Payload 注解的参数。类型为 Message 的参数在分析期间将被忽略。

默认情况下,推断的类型信息将覆盖入站 TypeId 和发送系统创建的相关头部。这允许接收系统自动转换为不同的域对象。这仅适用于参数类型是具体类型(不是抽象或接口),或者它来自 java.util 包。在所有其他情况下,将使用 TypeId 和相关头部。在某些情况下,您可能希望覆盖默认行为并始终使用 TypeId 信息。例如,假设您有一个 @RabbitListener 接受 Thing1 参数,但消息包含 Thing2,它是 Thing1 的子类(它是具体的)。推断的类型将不正确。要处理这种情况,请将 JacksonJsonMessageConverter 上的 TypePrecedence 属性设置为 TYPE_ID 而不是默认的 INFERRED。(该属性实际上在转换器的 DefaultJacksonJavaTypeMapper 上,但为了方便,在转换器上提供了 setter。)如果您注入自定义类型映射器,则应在映射器上设置该属性。
当从 Message 转换时,传入的 MessageProperties.getContentType() 必须符合 JSON(使用 contentType.contains("json") 进行检查)。从版本 2.2 开始,如果不存在 contentType 属性,或者它的默认值为 application/octet-stream,则假定为 application/json。要恢复到以前的行为(返回未转换的 byte[]),请将转换器的 assumeSupportedContentType 属性设置为 false。如果内容类型不受支持,则会发出 WARN 日志消息 Could not convert incoming message with content-type […​],并且 message.getBody() 按原样返回 — 作为 byte[]。因此,为了满足消费者端的 JacksonJsonMessageConverter 要求,生产者必须添加 contentType 消息属性 — 例如,作为 application/jsontext/x-json,或者使用会自动设置头部的 JacksonJsonMessageConverter。以下列表显示了许多转换器调用
@RabbitListener
public void thing1(Thing1 thing1) {...}

@RabbitListener
public void thing1(@Payload Thing1 thing1, @Header("amqp_consumerQueue") String queue) {...}

@RabbitListener
public void thing1(Thing1 thing1, o.s.amqp.core.Message message) {...}

@RabbitListener
public void thing1(Thing1 thing1, o.s.messaging.Message<Foo> message) {...}

@RabbitListener
public void thing1(Thing1 thing1, String bar) {...}

@RabbitListener
public void thing1(Thing1 thing1, o.s.messaging.Message<?> message) {...}

在前面列表中的前四种情况中,转换器尝试转换为 Thing1 类型。第五个示例无效,因为我们无法确定哪个参数应该接收消息负载。在第六个示例中,由于泛型类型是 WildcardType,因此适用 Jackson 默认值。

但是,您可以创建一个自定义转换器,并使用 targetMethod 消息属性来决定将 JSON 转换为哪种类型。

此类型推断只能在方法级别声明 @RabbitListener 注解时实现。对于类级别的 @RabbitListener,转换后的类型用于选择要调用的 @RabbitHandler 方法。因此,基础设施提供了 targetObject 消息属性,您可以在自定义转换器中使用它来确定类型。
从版本 1.6.11 开始,JacksonJsonMessageConverter,以及 DefaultJacksonJavaTypeMapperDefaultClassMapper),提供了 trustedPackages 选项,以克服 序列化小工具 漏洞。默认情况下,为了向后兼容,JacksonJsonMessageConverter 信任所有包——也就是说,它使用 * 作为该选项。

从 2.4.7 版本开始,转换器可以配置为在 Jackson 反序列化消息体后返回 Optional.empty(),如果 Jackson 返回 null。这有助于 @RabbitListener 以两种方式接收 null 负载

@RabbitListener(queues = "op.1")
void listen(@Payload(required = false) Thing payload) {
    handleOptional(payload); // payload might be null
}

@RabbitListener(queues = "op.2")
void listen(Optional<Thing> optional) {
    handleOptional(optional.orElse(this.emptyThing));
}

要启用此功能,请将 setNullAsOptionalEmpty 设置为 true;当为 false(默认)时,转换器将回退到原始消息体(byte[])。

@Bean
JacksonJsonMessageConverter converter() {
    JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
    converter.setNullAsOptionalEmpty(true);
    return converter;
}

反序列化抽象类

在 2.2.8 版本之前,如果 @RabbitListener 的推断类型是抽象类(包括接口),转换器将回退到在头部中查找类型信息,如果存在,则使用该信息;如果不存在,它将尝试创建抽象类。当使用配置有自定义反序列化器来处理抽象类的自定义 ObjectMapper,但传入消息具有无效类型头部时,这会导致问题。

从 2.2.8 版本开始,默认情况下保留以前的行为。如果您有这样的自定义 ObjectMapper,并且希望忽略类型头部,并始终使用推断类型进行转换,请将 alwaysConvertToInferredType 设置为 true。这是为了向后兼容性,并避免在转换失败(使用标准 ObjectMapper)时尝试转换的开销。

使用 Spring Data 投影接口

从 2.2 版本开始,您可以将 JSON 转换为 Spring Data Projection 接口,而不是具体的类型。这允许对数据进行非常选择性且低耦合的绑定,包括从 JSON 文档内的多个位置查找值。例如,以下接口可以定义为消息负载类型

interface SomeSample {

  @JsonPath({ "$.username", "$.user.name" })
  String getUsername();

}
@RabbitListener(queues = "projection")
public void projection(SomeSample in) {
    String username = in.getUsername();
    ...
}

默认情况下,访问器方法将用于在接收到的 JSON 文档中查找属性名称作为字段。@JsonPath 表达式允许自定义值查找,甚至可以定义多个 JSON 路径表达式,从多个位置查找值,直到某个表达式返回实际值。

要启用此功能,请在消息转换器上将 useProjectionForInterfaces 设置为 true。您还必须将 spring-data:spring-data-commonscom.jayway.jsonpath:json-path 添加到类路径中。

当用作 @RabbitListener 方法的参数时,接口类型会自动传递给转换器。

使用 RabbitTemplateMessage 转换

如前所述,类型信息在消息头部中传递,以协助转换器从消息转换。这在大多数情况下都运行良好。但是,当使用泛型类型时,它只能转换简单的对象和已知的“容器”对象(列表、数组和映射)。从 2.0 版本开始,JacksonJsonMessageConverter 实现了 SmartMessageConverter,这使得它可以与接受 ParameterizedTypeReference 参数的新 RabbitTemplate 方法一起使用。这允许转换复杂的泛型类型,如下例所示

Thing1<Thing2<Cat, Hat>> thing1 =
    rabbitTemplate.receiveAndConvert(new ParameterizedTypeReference<Thing1<Thing2<Cat, Hat>>>() { });

MarshallingMessageConverter

另一个选择是 MarshallingMessageConverter。它委托给 Spring OXM 库的 MarshallerUnmarshaller 策略接口的实现。您可以在此处阅读有关该库的更多信息。在配置方面,最常见的是只提供构造函数参数,因为 Marshaller 的大多数实现也实现了 Unmarshaller。以下示例展示了如何配置 MarshallingMessageConverter

<bean class="org.springframework.amqp.rabbit.core.RabbitTemplate">
    <property name="connectionFactory" ref="rabbitConnectionFactory"/>
    <property name="messageConverter">
        <bean class="org.springframework.amqp.support.converter.MarshallingMessageConverter">
            <constructor-arg ref="someImplementationOfMarshallerAndUnmarshaller"/>
        </bean>
    </property>
</bean>

JacksonXmlMessageConverter

此类在 2.1 版本中引入,可用于将消息转换为 XML 和从 XML 转换。

JacksonXmlMessageConverterJacksonJsonMessageConverter 具有相同的基类:AbstractJacksonMessageConverter

JacksonXmlMessageConverter 使用 Jackson 3.x 库。

您可以像使用 JacksonJsonMessageConverter 一样使用它,只是它支持 XML 而不是 JSON。以下示例配置了一个 JacksonJsonMessageConverter

<bean id="xmlConverterWithDefaultType"
        class="org.springframework.amqp.support.converter.JacksonXmlMessageConverter">
    <property name="classMapper">
        <bean class="org.springframework.amqp.support.converter.DefaultClassMapper">
            <property name="defaultType" value="foo.PurchaseOrder"/>
        </bean>
    </property>
</bean>

有关更多信息,请参阅 JacksonJsonMessageConverter

从版本 2.2 开始,如果不存在 contentType 属性,或者它的默认值为 application/octet-stream,则假定为 application/xml。要恢复到以前的行为(返回未转换的 byte[]),请将转换器的 assumeSupportedContentType 属性设置为 false

ContentTypeDelegatingMessageConverter

此类在 1.4.2 版本中引入,并允许根据 MessageProperties 中的内容类型属性委托给特定的 MessageConverter。默认情况下,如果没有 contentType 属性或其值与任何已配置的转换器都不匹配,则它会委托给 SimpleMessageConverter。以下示例配置了一个 ContentTypeDelegatingMessageConverter

<bean id="contentTypeConverter" class="ContentTypeDelegatingMessageConverter">
    <property name="delegates">
        <map>
            <entry key="application/json" value-ref="jsonMessageConverter" />
            <entry key="application/xml" value-ref="xmlMessageConverter" />
        </map>
    </property>
</bean>

Java 反序列化

本节介绍如何反序列化 Java 对象。

从不可信来源反序列化 Java 对象时可能存在漏洞。

如果您接受来自不可信来源的 content-typeapplication/x-java-serialized-object 的消息,您应该考虑配置允许反序列化的包和类。这适用于 SimpleMessageConverterSerializerMessageConverter,当它隐式或通过配置使用 DefaultDeserializer 时。

默认情况下,允许列表为空,这意味着不会反序列化任何类。

您可以设置一个模式列表,例如 thing1.thing1.thing2.Cat.MySafeClass

模式按顺序检查,直到找到匹配项。如果没有匹配项,则会抛出 SecurityException

您可以使用这些转换器上的 allowedListPatterns 属性设置模式。或者,如果您信任所有消息发起者,可以将环境变量 SPRING_AMQP_DESERIALIZATION_TRUST_ALL 或系统属性 spring.amqp.deserialization.trust.all 设置为 true

消息属性转换器

MessagePropertiesConverter 策略接口用于在 Rabbit 客户端 BasicProperties 和 Spring AMQP MessageProperties 之间进行转换。默认实现(DefaultMessagePropertiesConverter)通常足以满足大多数目的,但如果需要,您可以实现自己的。默认属性转换器将类型为 LongStringBasicProperties 元素转换为 String 实例,当大小不大于 1024 字节时。较大的 LongString 实例不会被转换(请参阅下一段)。此限制可以通过构造函数参数进行覆盖。

从版本 1.6 开始,超出长字符串限制(默认值:1024)的头部现在默认由 DefaultMessagePropertiesConverter 保留为 LongString 实例。您可以通过 getBytes[]toString()getStream() 方法访问其内容。

以前,DefaultMessagePropertiesConverter 将此类头部“转换”为 DataInputStream(实际上它只是引用了 LongString 实例的 DataInputStream)。在输出时,此头部未转换(除了转换为字符串——例如,通过在流上调用 toString() 得到的 java.io.DataInputStream@1d057a39)。

现在,大型入站 LongString 头部在输出时也能正确地“转换”(默认情况下)。

提供了一个新的构造函数,允许您配置转换器以像以前一样工作。以下列表显示了该方法的 Javadoc 注释和声明

/**
 * Construct an instance where LongStrings will be returned
 * unconverted or as a java.io.DataInputStream when longer than this limit.
 * Use this constructor with 'true' to restore pre-1.6 behavior.
 * @param longStringLimit the limit.
 * @param convertLongLongStrings LongString when false,
 * DataInputStream when true.
 * @since 1.6
 */
public DefaultMessagePropertiesConverter(int longStringLimit, boolean convertLongLongStrings) { ... }

同样从版本 1.6 开始,MessageProperties 中添加了一个名为 correlationIdString 的新属性。以前,在与 RabbitMQ 客户端使用的 BasicProperties 之间进行转换时,执行了不必要的 byte[] <→ String 转换,因为 MessageProperties.correlationIdbyte[],但 BasicProperties 使用 String。(最终,RabbitMQ 客户端使用 UTF-8 将 String 转换为字节以放入协议消息中)。

为了提供最大的向后兼容性,已向 DefaultMessagePropertiesConverter 添加了一个名为 correlationIdPolicy 的新属性。它接受 DefaultMessagePropertiesConverter.CorrelationIdPolicy 枚举参数。默认情况下,它设置为 BYTES,这复制了以前的行为。

对于入站消息

  • STRING:仅映射 correlationIdString 属性

  • BYTES:仅映射 correlationId 属性

  • BOTH:映射两个属性

对于出站消息

  • STRING:仅映射 correlationIdString 属性

  • BYTES:仅映射 correlationId 属性

  • BOTH:同时考虑两个属性,其中 String 属性优先

同样从版本 1.6 开始,入站 deliveryMode 属性不再映射到 MessageProperties.deliveryMode。它改为映射到 MessageProperties.receivedDeliveryMode。此外,入站 userId 属性不再映射到 MessageProperties.userId。它改为映射到 MessageProperties.receivedUserId。这些更改是为了避免在同一 MessageProperties 对象用于出站消息时这些属性的意外传播。

从 2.2 版本开始,DefaultMessagePropertiesConverter 使用 getName() 而不是 toString() 转换类型为 Class<?> 的任何自定义头部值;这避免了消费应用程序必须从 toString() 表示中解析类名。对于滚动升级,您可能需要更改消费者以理解两种格式,直到所有生产者都升级。

© . This site is unofficial and not affiliated with VMware.