RabbitMQ消息为什么变成数字了呢?

结论
(1)消息变成数字,是因为没有找到合适的converter

缘起


需要监听兄弟团队一个RabbitMQ队列。这种监听第三方队列的操作都在一个项目,直接把已有的监听代码copy过来【这样比较快】,把不需要的删除,譬如处理逻辑。

    @RabbitListener(queues = PRODUCT_SHELF_STATUS)
    public void handleShelfStatusChange(String msg,
                                        @Header(AmqpHeaders.CONTENT_TYPE) String contentType) {
        log.info("商品上下架消息 data:{} contentType:{}", JSON.toJSONString(msg), contentType);
        try {
            handleMsg(msg);
            return;
        } catch (Exception e) {
            log.warn("商品上下架消息 报错了 msg:{} {}", msg, e.getMessage());
        }
        sendToLogQueueWhenFail(msg);
    }

 

自测。。。

报错了?打断点看一看

奇怪,发的消息明明是字符串,为什么变成数字了。

 

BugShooting:站到巨人的肩膀上

 

搜索了下,居然找到相同的报错。原来是MessageConverter缺失,并看到了解决方案:

核对了下项目,的确没有配Jackson2JsonMessageConverter
但之前的消息监听不都跑得好好的,为什么呢?

 

BugShooting:Debug【必杀技

 

Debug后,找到关键代码:

如果没有配置MessageConverter,MessagingMessageListenerAdapter使用的MessagingMessageConverter会初始化一个SimpleMessageConverter

org.springframework.amqp.support.converter.MessagingMessageConverter#MessagingMessageConverter()

org.springframework.amqp.support.converter.SimpleMessageConverter#fromMessage其实,还有个地方比较关键,这个在后面讲。小提示:spring-amqp的版本号
如此说来以前,之前监听消息的contentType可能是默认的text/plain
这样的话,还不能按搜到的方案改。这会导致老逻辑都报错,因为Jackson2JsonMessageConverter只会处理contetType contain json 字符串的Message

org.springframework.amqp.support.converter.Jackson2JsonMessageConverter#fromMessage(org.springframework.amqp.core.Message, java.lang.Object)
如何优雅地改呢?
SpringBoot的亮点就是自动配置、起步依赖。那么有没有一个配置参数,配置一下,contentType 对应的MessageCoverter都有了呢?
 先看下SpringBoot对Rabbit的自动配置:

org.springframework.boot.autoconfigure.amqp.RabbitAnnotationDrivenConfiguration#RabbitAnnotationDrivenConfiguration


那么改动就小的改法就有了,直接把期望的MessageConverter初始化到Spring容器中就可以了。

    @Bean
    public MessageConverter messageConverter() {
        ContentTypeDelegatingMessageConverter messageConverter = new ContentTypeDelegatingMessageConverter();
        messageConverter.addDelegate(MediaType.APPLICATION_JSON_VALUE, new Jackson2JsonMessageConverter());
        messageConverter.addDelegate(MediaType.TEXT_PLAIN_VALUE, new SimpleMessageConverter());
        return messageConverter;
    }

 

配置多个MessageConverter,需要借助ContentTypeDelegatingMessageConverter

org.springframework.amqp.support.converter.ContentTypeDelegatingMessageConverter
重启应用,看看上面的配置是否生效。

可以看到,已经生效了。

org.springframework.amqp.rabbit.listener.adapter.AbstractAdaptableMessageListener#extractMessage
新的报错

这个错误比较熟悉,将监听RabbitMQ消息的Argument改为对应的Java DTO就可以了

    @RabbitListener(queues = PRODUCT_SHELF_STATUS)
    public void handleShelfStatusChange(List<ShelfStatusProductMQDTO> msg,
                                        Message message,
                                        @Header(AmqpHeaders.CONTENT_TYPE) String contentType) {
        log.info("商品上下架消息 data:{} contentType:{}", JSON.toJSONString(msg), contentType);
        try {
            handleMsg(msg);
            return;
        } catch (Exception e) {
            log.warn("商品上下架消息 报错了 msg:{} {}", msg, e.getMessage());
        }
//        sendToLogQueueWhenFail(msg);
    }

 


至此,问题完美解决。

但是,之前收到的数字到底是什么呢?

收到消息的数字是什么呢?

将RabbitMQ Message中payload的byte[]中的数字,使用英文逗号拼成的字符串小贴士:
Arrays.stream(ObjectUtils.toObjectArray(message.getBody())).map(String::valueOf).collect(Collectors.joining(","))

    @Test
    public void testNumberMqMsg() throws IOException {
        String msg = "{\"name\":\"mq\",\"type\":1}";
        byte[] msgByte = msg.getBytes(StandardCharsets.UTF_8.name());
        String msgByteStr = Arrays.stream(ObjectUtils.toObjectArray(msgByte)).map(String::valueOf).collect(Collectors.joining(","));
        String[] msgBytes = msgByteStr.split(",");
        byte[] bytes = new byte[msgBytes.length];
        for (int i = 0; i < msgBytes.length; i++) {
            bytes[i] = Byte.parseByte(msgBytes[i]);
        }
        assertThat(msg).isEqualTo(new String(bytes, StandardCharsets.UTF_8.name()));
    }

 

为什么是这样呢,这个在下面有分析

 

发送String类型的消息,默认的消息类型是什么?

amqpTemplate.convertAndSend(RabbitMQEnum.TEST.getExchange(), RabbitMQEnum.TEST.getRoutingKey(), JSON.toJSONString(msg))

org.springframework.amqp.rabbit.core.RabbitTemplate#convertMessageIfNecessary

org.springframework.amqp.support.converter.SimpleMessageConverter#createMessage

 

福利:

本想写个demo,方便小伙伴研究。

但异常没有复现

 

原来spring-amqp自5.1.2开始已经对这个点进行了优化,即不需要配置额外的MessageConverter,原因在之后的resolveArgument环节,匹配到了RabbitListenerAnnotationBeanPostProcessor$BytesToStringConverter。这个Converter就可以将String类型Payload的byte[]可以正常convert为String字符串。

参数解析代码入口:

org.springframework.messaging.handler.invocation.InvocableHandlerMethod#getMethodArgumentValues

org.springframework.amqp.rabbit.annotation.RabbitListenerAnnotationBeanPostProcessor$BytesToStringConverter

 

org.springframework.core.convert.support.GenericConversionService#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)

再看有异常项目中spring amqp的版本是2.0.3.RELEASE

在resolve时,匹配到的converter是ArrayToStringConverter。

org.springframework.core.convert.support.GenericConversionService.ConvertersForPair#getConverter

org.springframework.core.convert.support.ArrayToStringConverter#convert

 

show the code :
https://github.com/helloworldtang/mq

 

小结:

1、建议还是对不同的contentType配置特定的MessageConverter,这样有两个好处
(1)代码简洁
(2)提升性能。一步到位,避免额外的数据类型转换。
2、上面分析后的汇总

(1)RabbitMQ在Spring Boot的RabbitAutoConfiguration没有配置MessageConverter。
(2)spring-amqp在处理RabbitMQ消息时,会根据contentType来选择不同的MessageConverter来执行解码操作。

(3)spring-amqp的消息解码组件MessagingMessageListenerAdapter有一个可以处理contentType为text/plain、text/xml等的Message。
(4)spring-amqp在发送String类型的消息时,默认的contentType是text/plain。


参考:
https://www.jianshu.com/p/83861676051c

 

拓展阅读:
惊人!Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
当@Transactional遇到@CacheEvict,会不会先清缓存呢?

 

What does the class class [B represents in Java?
I am trying out a tool jhat here to test my java memory usage. It reads in a heap dump file and prints out information as html. However, the tables shows as follows:

Class Instance Count Total Size
class [B 36585 49323821
class [Lcom.sun.mail.imap.IMAPMessage; 790 16254336
class [C 124512 12832896
class [I 23080 11923504
class [Ljava.lang.Object; 13614 6664528
class java.lang.String 108982 2179640
class java.lang.Integer 219502 878008

What are those [B [C etc classes?
Those are arrays of primitives ([B == byte[], [C == char, [I == int). [Lx; is an array of class type x.

For a full list:
[Z = boolean
[B = byte
[S = short
[I = int
[J = long
[F = float
[D = double
[C = char
[L = any non-primitives(Object)

Also see the Javadoc for Class.getName.
https://docs.oracle.com/javase/9/docs/api/java/lang/Class.html#getName--
public String getName​()
Returns the name of the entity (class, interface, array class, primitive type, or void) represented by this Class object, as a String.
If this class object represents a reference type that is not an array type then the binary name of the class is returned, as specified by The Java™ Language Specification.

If this class object represents a primitive type or void, then the name returned is a String equal to the Java language keyword corresponding to the primitive type or void.

If this class object represents a class of arrays, then the internal form of the name consists of the name of the element type preceded by one or more '[' characters representing the depth of the array nesting. The encoding of element type names is as follows:

Element Type Encoding
boolean Z
byte B
char C
class or interface Lclassname;
double D
float F
int I
long J
short S
The class or interface name classname is the binary name of the class specified above.

Examples:

String.class.getName()
returns "java.lang.String"
byte.class.getName()
returns "byte"
(new Object[3]).getClass().getName()
returns "[Ljava.lang.Object;"
(new int[3][4][5][6][7][8][9]).getClass().getName()
returns "[[[[[[[I"

Returns:
the name of the class or interface represented by this object.

https://stackoverflow.com/questions/1466508/what-does-the-class-class-b-represents-in-java

 

posted @ 2020-04-01 19:22  沧海一滴  阅读(4172)  评论(0编辑  收藏  举报