結論:
(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