跳到主要内容
版本:4.1.1

消息转换器

Doubao 中英对照 Message Converters

AmqpTemplate 还定义了若干发送和接收消息的方法,它们委托给 MessageConverter。MessageConverter 为每个方向提供一个方法:一个用于转换 为 Message,另一个用于 从 Message 转换。注意,转换为 Message 时,除对象外还可以提供属性。object 参数通常对应消息体。下面的清单展示了 MessageConverter 接口的定义:

public interface MessageConverter {

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

Object fromMessage(Message message) throws MessageConversionException;

}

AmqpTemplate 上相关的 Message-发送方法比我们前面讨论的方法更简单,因为它们不需要 Message 实例。相反,MessageConverter 负责通过把提供的对象转换为消息体的字节数组、再添加任何提供的 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;

在接收一侧,只有两个方法:一个接收队列名,另一个依赖模板的“queue”属性已被设置。下面的清单展示了这两个方法的定义:

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 直接返回消息体内容作为字节数组。

重要信息参见 Java 反序列化。

转换为 Message​

从任意 Java Object 转换为 Message 时,SimpleMessageConverter 同样处理字节数组、字符串和可序列化实例。它把这些都转换为字节(对于字节数组,无需转换),并相应设置 content-type 属性。如果要转换的 Object 不匹配这些类型之一,消息体为 null。

SerializerMessageConverter​

此转换器与 SimpleMessageConverter 类似,区别在于它可以配置为使用其他 Spring Framework Serializer 和 Deserializer 实现来处理 application/x-java-serialized-object 转换。

重要信息参见 Java 反序列化。

JacksonJsonMessageConverter​

本节介绍如何使用 JacksonJsonMessageConverter 进行 Message 的双向转换。它包含以下小节:

备注

AbstractJackson2MessageConverter 及其实现和相关的 Jackson2JavaTypeMapper API 已被弃用,将在 4.0 版本移除,取而代之的是基于 Jackson 3 的相应类。各自的迁移指南参见这些被弃用类的 Javadoc。

转换为 Message​

如上一节所述,通常不建议依赖 Java 序列化。一种相当常见、更灵活且能跨不同语言和平台移植的替代方案是 JSON(JavaScript Object Notation)。可以在任何 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 默认设置来转换 JSON(通常是 map)。

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

important

默认情况下,推断的类型信息会覆盖发送系统创建的入站 _TypeId_ 及相关头。这让接收系统可以自动转换为不同的领域对象。这仅在参数类型是具体类型(非抽象类或接口),或它来自 java.util 包时适用。在所有其他情况下,使用 _TypeId_ 及相关头。有些情况下你可能希望覆盖默认行为,始终使用 _TypeId_ 信息。例如,假设你有一个接收 Thing1 参数的 @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/json 或 text/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 消息属性,你可以在自定义转换器中用它确定类型。

important

从 1.6.11 版本开始,JacksonJsonMessageConverter 以及 DefaultJacksonJavaTypeMapper(DefaultClassMapper)提供了 trustedPackages 选项,以应对序列化 Gadget 漏洞。默认情况下,JacksonJsonMessageConverter 只信任 java.util 和 java.lang 包。可以使用 * 信任所有。

从 2.4.7 版本开始,如果 Jackson 在反序列化消息体后返回 null,可以配置转换器返回 Optional.empty()。这让 @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 投影接口,而不是具体类型。这允许对数据进行非常有选择性、低耦合的绑定,包括从 JSON 文档内部多个位置查找值。例如,可以定义下面的接口作为消息负载类型:

interface SomeSample {

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

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

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

要启用此功能,在消息转换器上把 useProjectionForInterfaces 设置为 true。你还必须把 spring-data:spring-data-commons 和 com.jayway.jsonpath:json-path 添加到类路径。

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

用 RabbitTemplate 从 Message 转换​

如前所述,类型信息通过消息头传达,以在从消息转换时协助转换器。这在大多数情况下工作良好。不过,使用泛型类型时,它只能转换简单对象和已知的“容器”对象(列表、数组和 map)。从 2.0 版本开始,JacksonJsonMessageConverter 实现了 SmartMessageConverter,使其可以与接收 ParameterizedTypeReference 参数的新 RabbitTemplate 方法一起使用。这允许转换复杂泛型类型,如下例所示:

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

MarshallingMessageConverter​

另一个选项是 MarshallingMessageConverter。它委托给 Spring OXM 库中 Marshaller 和 Unmarshaller 策略接口的实现。你可以在这里阅读更多关于该库的内容。在配置方面,最常见的是只提供构造参数,因为大多数 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 之间转换消息。

JacksonXmlMessageConverter 和 JacksonJsonMessageConverter 具有相同的基类: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 对象。

important

从未受信任来源反序列化 Java 对象时存在潜在漏洞。

如果你接受来自未受信任来源、content-type 为 application/x-java-serialized-object 的消息,应考虑配置允许反序列化哪些包和类。这同时适用于 SimpleMessageConverter 和 SerializerMessageConverter——当后者被配置为(隐式或通过配置)使用 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)通常足以满足大多数用途,但如有需要你也可以实现自己的转换器。默认属性转换器会把 LongString 类型的 BasicProperties 元素在大小不超过 1024 字节时转换为 String 实例。更大的 LongString 实例不转换(见下一段)。可以通过构造参数覆盖此限制。

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

以前,DefaultMessagePropertiesConverter 会把此类头“转换”为 DataInputStream(实际上它只是引用了 LongString 实例的 DataInputStream)。在输出时,这个头不会被转换(除非转换为 String——例如通过对流调用 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.correlationId 是 byte[],而 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() 表示中解析类名。对于滚动升级,你可能需要修改消费者以理解两种格式,直到所有生产者都升级。