问题

最近在使用RabbitMq时遇到了一个问题,明明是转换成json发送到mq中的数据,消费者接收到的却是一串数字也就是byte数组,但是使用mq可视化页面查看数据却是正常的,之前在使用过程中从未遇到过这种情况,遇到的情况如下所示:

生产者发送消息的代码如下所示:

public void sendJsonStrMsg(String jsonStr){
rabbitTemplate.convertAndSend(JSON_QUEUE, jsonStr);
}

消费者代码如下所示:

@RabbitHandler
@RabbitListener(queuesToDeclare = {@Queue(name=ProducerService.JSON_QUEUE, durable = "true")},containerFactory = "prefetchTenRabbitListenerContainerFactory")
public void listenJsonMsg(String msg, Channel channel, Message message){
log.debug("json字符串类型消息>>>>{}",msg);
}

引入的containerFactory如下所示:

@Bean
public RabbitListenerContainerFactory<SimpleMessageListenerContainer> prefetchTenRabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
MessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(); //<x>
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(jackson2JsonMessageConverter);
return factory;
}

注意代码中标有<x>的地方,这里就是我们解决问题的关键。

解决方案

我们先说解决方案,再说原因,解决方案其实很简单,在保持上述代码不变的情况下,只需要再注入如下的bean即可:

@Bean
public MessageConverter jackson2JsonMessageConverter(){
return new Jackson2JsonMessageConverter("*");
}

解决方案就是这么简单,只需要在原来的代码的基础上注入Jackson2JsonMessageConverter就可以了,但是原理是什么呢?且往后看。

原理分析

关于原理的解释我们从源码层面来说,毕竟源码面前没有秘密.

生产者源码分析

首先看我们发送消息到mq的方法rabbitTemplate.convertAndSend(JSON_QUEUE, jsonStr),从此方法进去后,经过重载的方法后最终到达下面所示的方法:

@Override
public void convertAndSend(String exchange, String routingKey, final Object object,
@Nullable CorrelationData correlationData) throws AmqpException { send(exchange, routingKey, convertMessageIfNecessary(object), correlationData);
}

着重看convertMessageIfNecessary方法,方法名已经很直白的告诉我们了,如果需要就转换消息,我们点进去看一下这个方法:

protected Message convertMessageIfNecessary(final Object object) {
if (object instanceof Message) { //<1>
return (Message) object;
}
return getRequiredMessageConverter().toMessage(object, new MessageProperties()); //<2>
}

<1>处是说如果要发送到mq的对象是Message的实例,那么就直接转换成Message类型返回,否则就获取MessageConverter后调用toMessage()方法返回Message对象。

我们先看一下RabbitTemplate#getRequiredMessageConverter(),如下所示:

private MessageConverter getRequiredMessageConverter() throws IllegalStateException {
MessageConverter converter = getMessageConverter();
if (converter == null) {
throw new AmqpIllegalStateException(
"No 'messageConverter' specified. Check configuration of RabbitTemplate.");
}
return converter;
} public MessageConverter getMessageConverter() {
return this.messageConverter; //<1>
}

<1>处的代码表明需要一个messageConverter对象,我在RabbitTemplate源码中找到了对应的set方法,由于我们没有调用set方法取设置messageConverter的值,那么就需要取查找默认值,默认值的设置如下代码所示:

/**
* Convenient constructor for use with setter injection. Don't forget to set the connection factory.
*/
public RabbitTemplate() {
initDefaultStrategies(); // NOSONAR - intentionally overridable; other assertions will check
} /**
* Set up the default strategies. Subclasses can override if necessary.
设置默认策略,子类在必须的时候可以重写
*/
protected void initDefaultStrategies() {
setMessageConverter(new SimpleMessageConverter());
} public void setMessageConverter(MessageConverter messageConverter) {
this.messageConverter = messageConverter;
}

我们点进去SimpleMessageConverter#toMessage()方法看一下是如何将一个java对象转换成Message对象的,可惜的是在SimpleMessageConverter中未找到toMessage方法,我们在此先看一下SimpleMessageConverter继承情况,类图如下:

去掉了一些无用的接口和类之后,剩下的类图如下所示,沿着类图向上找,在AbstractMessageConverter中找到了toMessage方法:

@Override
public final Message toMessage(Object object, @Nullable MessageProperties messagePropertiesArg,
@Nullable Type genericType)
throws MessageConversionException { MessageProperties messageProperties = messagePropertiesArg;
if (messageProperties == null) {
messageProperties = new MessageProperties();
}
Message message = createMessage(object, messageProperties, genericType); //<1>
messageProperties = message.getMessageProperties();
if (this.createMessageIds && messageProperties.getMessageId() == null) {
messageProperties.setMessageId(UUID.randomUUID().toString());
}
return message;
}

该方法中没有我们需要的内容,继续看<1>处的方法,该方法需要返回到SimpleMessageConverter中:

@Override
protected Message createMessage(Object object, MessageProperties messageProperties) throws MessageConversionException {
byte[] bytes = null;
if (object instanceof byte[]) { //<1>
bytes = (byte[]) object;
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_BYTES); //<1.x>
}
else if (object instanceof String) { //<2>
try {
bytes = ((String) object).getBytes(this.defaultCharset);
}
catch (UnsupportedEncodingException e) {
throw new MessageConversionException(
"failed to convert to Message content", e);
}
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);//<2.x>
messageProperties.setContentEncoding(this.defaultCharset);
}
else if (object instanceof Serializable) { //<3>
try {
bytes = SerializationUtils.serialize(object);
}
catch (IllegalArgumentException e) {
throw new MessageConversionException(
"failed to convert to serialized Message content", e);
}
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT);//<3.x>
}
if (bytes != null) {
messageProperties.setContentLength(bytes.length);
return new Message(bytes, messageProperties);
}
throw new IllegalArgumentException(getClass().getSimpleName()
+ " only supports String, byte[] and Serializable payloads, received: " + object.getClass().getName()); //<4>
}

这个方法就比较有意思了,在<1><2><3>三处分别判断了发送的消息是否是byte[]StringSerializable,并且在判断之后将消息的content_type属性分别设置为application/octet-streamtext/plainapplication/x-java-serialized-object三种类型,除了以上三种类型之外的数据将被抛出异常,很显然我们前面发送的是字符串消息,那么content_type属性的值必定是text/plain了,可以在mq可视化页面上看到:

经过以上的步骤,java对象已经转换成Message对象并且发送到了MQ中,下面就是消费者的源码分析了。

消费者源码分析

本来想从SpringBoot启动开始到Bean加载、注册一直到获取消息的源码分析下来的,奈何IoC和AOP的源码还没看完,实在是心有余而力不足,此处留个坑待以后再战。

前面生产者是调用MessageConverter.toMessage()方法将java对象转换成Message对象发送到MQ中的,那么消费者应该是反其道而行之,调用MessageConverter.formMessage()方法将Message对象转换成java对象,我们就从formMessage方法开始看,生产者使用的是SimpleMessageConverter,那么此处还是查看此类的fromMessage方法:

/**
* Converts from a AMQP Message to an Object.
*/
@Override
public Object fromMessage(Message message) throws MessageConversionException {
Object content = null;
MessageProperties properties = message.getMessageProperties();
if (properties != null) {
String contentType = properties.getContentType();//<1>
if (contentType != null && contentType.startsWith("text")) { //<2>
String encoding = properties.getContentEncoding();
if (encoding == null) {
encoding = this.defaultCharset;
}
try {
content = new String(message.getBody(), encoding);
}
catch (UnsupportedEncodingException e) {
throw new MessageConversionException(
"failed to convert text-based Message content", e);
}
}
else if (contentType != null &&
contentType.equals(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT)) { //<3>
try {
content = SerializationUtils.deserialize(
createObjectInputStream(new ByteArrayInputStream(message.getBody()), this.codebaseUrl));
}
catch (IOException | IllegalArgumentException | IllegalStateException e) {
throw new MessageConversionException(
"failed to convert serialized Message content", e);
}
}
}
if (content == null) {
content = message.getBody(); //<4>
}
return content;
}

以上代码很容易理解

<1>处是从消息属性中获取到消息的content_type属性

<2>处和<3>处则是分别判断是否text/plain以及application/x-java-serialized-object

如果以上两种都不符合,那么只能是调用message.getBody()返回一个byte[]类型的byte数组,这也就是文章开头返回一串数字的由来。

问题解决

虽然消费者源码分析得到了一个返回一串数字的缘由,但是这并不是造成本次问题的根本原因,我们再回顾一下问题中消费者所使用的一个containerFactory

@Bean
public RabbitListenerContainerFactory<SimpleMessageListenerContainer> prefetchTenRabbitListenerContainerFactory() {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
MessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(); //<1>
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(jackson2JsonMessageConverter); //<2>
return factory;
}

<1><2>处使用的messageConver是Jackson2JsonMessageConverter,通过前面类图我们可以知道它也是实现了MessageConvert接口,我们看一下这个类的源码:

/**
* JSON converter that uses the Jackson 2 Json library.
*/
public class Jackson2JsonMessageConverter extends AbstractJackson2MessageConverter { public Jackson2JsonMessageConverter() {
this("*");
} public Jackson2JsonMessageConverter(String... trustedPackages) {
this(new ObjectMapper(), trustedPackages);
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
} public Jackson2JsonMessageConverter(ObjectMapper jsonObjectMapper) {
this(jsonObjectMapper, "*");
} public Jackson2JsonMessageConverter(ObjectMapper jsonObjectMapper, String... trustedPackages) {
super(jsonObjectMapper, MimeTypeUtils.parseMimeType(MessageProperties.CONTENT_TYPE_JSON), trustedPackages); //<1>
} }

我删掉了一些无用的代码以及注释,可以在类注释上很显然的看到这个转换器是使用jackson的JSON转换器,也就是说这个转换器只对json数据有效,该类中并没有找到fromMessage和toMessage方法,那么只能从其父类AbstractJackson2MessageConverter中查找fromMessage方法,如下所示,注意上面代码中<1>的地方,传递的content_type类型是application/json

// AbstractJackson2MessageConverter

@Override
public Object fromMessage(Message message, @Nullable Object conversionHint) throws MessageConversionException {
Object content = null;
MessageProperties properties = message.getMessageProperties();
if (properties != null) {
String contentType = properties.getContentType();//<1>
//supportedContentType即为构造函数中传递的MimeType
if (contentType != null && contentType.contains(this.supportedContentType.getSubtype())) {//<2>
String encoding = properties.getContentEncoding();
if (encoding == null) {
encoding = getDefaultCharset();
}
try {
if (conversionHint instanceof ParameterizedTypeReference) {
content = convertBytesToObject(message.getBody(), encoding,
this.objectMapper.getTypeFactory().constructType(
((ParameterizedTypeReference<?>) conversionHint).getType()));
}
else if (getClassMapper() == null) {
JavaType targetJavaType = getJavaTypeMapper()
.toJavaType(message.getMessageProperties());
content = convertBytesToObject(message.getBody(),
encoding, targetJavaType);
}
else {
Class<?> targetClass = getClassMapper().toClass(// NOSONAR never null
message.getMessageProperties());
content = convertBytesToObject(message.getBody(),
encoding, targetClass);
}
}
catch (IOException e) {
throw new MessageConversionException(
"Failed to convert Message content", e);
}
}
else {
if (this.log.isWarnEnabled()) {
this.log.warn("Could not convert incoming message with content-type ["
+ contentType + "], '" + this.supportedContentType.getSubtype() + "' keyword missing."); //<3>
}
}
}
if (content == null) {
content = message.getBody();
}
return content;
}

上述代码可以这么理解,Jackson2JsonMessageConverter初始化时将json格式的content_type传递到父类AbstractJackson2MessageConverter中,当消费者将Message消息转换为Java对象时实际上是调用的AbstractJackson2MessageConverter#fromMessage()方法,由于该方法只支持json格式的content_type,因此执行了<3>处的代码,打印出了文章开头所示的提示信息。

因此最终的解决方案其实有2种

1.发送消息时也使用Jackson2JsonMessageConverter,这种方式用来支持json格式的数据传输;

2.删除containerFactory中设置的MessageConvert,使用默认的SimpleMessageConverter,这样就只支持字符串、byte数组以及序列化对象三种消息了。

Springboot集成RabbitMQ之MessageConvert源码解析的更多相关文章

  1. 深度理解springboot集成cache缓存之源码解析

    一.案例准备 1.创建数据表(employee表) 2.创建Employee实体类封装数据库中的数据 @AllArgsConstructor @NoArgsConstructor @Data @ToS ...

  2. 详解SpringBoot集成jsp(附源码)+遇到的坑

    本文介绍了SpringBoot集成jsp(附源码)+遇到的坑 ,分享给大家 1.大体步骤 (1)创建Maven web project: (2)在pom.xml文件添加依赖: (3)配置applica ...

  3. SpringBoot集成jsp(附源码)+遇到的坑

    1.大体步骤 (1)       创建Maven web project: (2)       在pom.xml文件添加依赖: (3)       配置application.properties支持 ...

  4. SpringBoot(1.5.6.RELEASE)源码解析

    转自 https://www.cnblogs.com/dylan-java/p/7450914.html 启动SpringBoot,需要在入口函数所在的类上添加@SpringBootApplicati ...

  5. springboot ---> spring ioc 注册流程 源码解析 this.prepareContext 部分

    现在都是在springboot 中 集成 spirng,那我们就从springboot 开始. 一:springboot 启动main 函数 public static void main(Strin ...

  6. springboot自动扫描添加的BeanDefinition源码解析

    1. springboot启动过程中,首先会收集需要加载的bean的定义,作为BeanDefinition对象,添加到BeanFactory中去. 由于BeanFactory中只有getBean之类获 ...

  7. 26. SpringBoot 初识缓存及 SimpleCacheConfiguration源码解析

    1.引入一下starter: web.cache.Mybatis.MySQL @MapperScan("com.everjiankang.cache.dao") @SpringBo ...

  8. SpringBoot之DispatcherServlet详解及源码解析

    在使用SpringBoot之后,我们表面上已经无法直接看到DispatcherServlet的使用了.本篇文章,带大家从最初DispatcherServlet的使用开始到SpringBoot源码中Di ...

  9. springboot源码解析-管中窥豹系列之web服务器(七)

    一.前言 Springboot源码解析是一件大工程,逐行逐句的去研究代码,会很枯燥,也不容易坚持下去. 我们不追求大而全,而是试着每次去研究一个小知识点,最终聚沙成塔,这就是我们的springboot ...

随机推荐

  1. UCOS 多任务系统中需要注意的地方 一个任务至少要有一些执行内容

    图片说明: 在一个TASK的最外层FOR();中  如果有调用BREAK会怎样??

  2. Centos6.5 修改主机名(hostname)

    centos6需要修改两处:一处是/etc/sysconfig/network,另一处是/etc/hosts,只修改任一处会导致系统启动异常.首先切换到root用户. /etc/sysconfig/n ...

  3. 三大主流开源硬件对比:Arduino vs Raspberry Pi vs BeagleBone

    http://www.elecfans.com/emb/361236_3.html 下文摘自上面的链接 软硬件整合是今年一再被提及的话题,如今我们也可以看到不少硬件创业的成功案例,比如Jawbone ...

  4. minikube与kubectl版本不一致问题

    在使用minikube学习的时候,会出现你在mac上安装的minikube默认是最新版本,但是实际情况是需要安装低一些版本.可以按照k8s官方文档安装的时候指定不同安装版本,如果很慢或者无法访问直接走 ...

  5. MyBatis-框架使用和分析

      一.基础知识 MyBatis 是一款优秀的持久层框架,它支持自定义 SQL.存储过程以及高级映射.MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作.MyBatis 可 ...

  6. C#解决WebClient不能下载https网页内容

    在下载之前,执行以下代码即可: if (stUrl.Substring(0, 5) == "https") { // 解决WebClient不能通过https下载内容问题 Serv ...

  7. setTimeout使用问题

    通常禁止使用setTimeout的情况: 1.不能用于模拟异步,有的人不熟悉异步流程,而使用setTimeout伪实现,比如设置一个 setTimeout 来等待函数执行完毕,正确做法是使用回调来处理 ...

  8. 机器学习PAL数据预处理

    机器学习PAL数据预处理 本文介绍如何对原始数据进行数据预处理,得到模型训练集和模型预测集. 前提条件 完成数据准备,详情请参见准备数据. 操作步骤 登录PAI控制台. 在左侧导航栏,选择模型开发和训 ...

  9. HiLink & LiteOS & IoT芯片 让IoT开发简单高效

    HiLink & LiteOS & IoT芯片让IoT开发简单高效 华为HiLink & LiteOS & IoT芯片使能三件套,让IoT开发更简单高效.下一代智能手机 ...

  10. cuGraph-GPU图形分析

    cuGraph-GPU图形分析 所述RAPIDS cuGraph库是GPU的集合加速图形算法,在GPU DataFrames中发现过程数据.cuGraph的愿景是使图分析无处不在,以至于用户只是根据分 ...