温馨提示:图片看不清按ctrl+鼠标滚动放大网页
- 问题描述
2020年11月28号,rabbitmq队列visitLog一直处于阻塞状态,重启consumer,抛出org.springframework.amqp.AmqpException: No method found for class [B。临时解决方案,将@RabbitListener注解移动到方法上:
按照上图方法操作后,consumer能够消费消息,但有500多条消息消费错误,消费者在接受消息时,其中有的消息为字节数组
初次判断原因为前端发送请求参数出现了错误,在项目中,有个埋点报了如下错误:
因此,定下结论,前端发送了图上参数埋点,导致了rabbitmq,visitLog阻塞。
2020年12月4号,线上环境consumer消费异常,从11:30开始,不断发送错误邮件
consumer为什么接受的消息莫名其妙变成了数字呢?
2. 问题跟踪
停掉消费者,通过rabbitmq management查看,mq所接受到的消息:
如上图红框所示,content-type变成了application/json ,以前一直是text/plain。查看consumer端的代码:
接受消息为字符串类型,原因就是mq里所存的消息content-type是application/json,而consumer接受的消息为字符串,没有对消息进行转换,即配置额外的MessageConverter。通过查阅文档,
spring-amqp自5.1.2开始已经对这个点进行了优化,即不需要配置额外的MessageConverter,原因在之后的resolveArgument环节,匹配到了RabbitListenerAnnotationBeanPostProcessor$BytesToStringConverter。这个Converter就可以将String类型Payload的byte[]可以正常convert为String字符串。 |
在周五临时解决方案中,升级springboot版本,并StringEscapeUtils.unescapeJava,处理掉字符串中的转义字符,最终临时上线。但是后面一直出现json解析错误。
3. 问题原因
问题的根本原因就是消息的发送方发送消息content-type从原来的text/plain变成了application/json,并且当天临时上线解决后,content-type又变成了text/plain,而consumer只能消费text/plain的消息。
4. 代码追踪
4.1 生产者代码追踪
查看visitLog消息的生产者:
点击convertAndSend分析,直到doSend方法浏览代码:
如上图所示:红框中有message.getMessageProperties(),获取消息属性,那消息属性是在哪里设置的呢?
查看amqpTemplate配置
查看RabbitTemplate类源码,浏览属性:
浏览方法:
看到RabbitTemplate的默认MessageConverter是SimpleMessageConverter: 点击SimpleMessageConverter源码:
查看createMessage方法注释,创建一个AMQP消息,从生产者; 如果消息类型为字节数组,则content-type为application/octet-stream; 如果消息类型为字符串,则content-type为text/plain; 如果消息类型为序列化对象,则content-type为application/x-java-serialized-object。 查看convertAndSend发送的类型:
为字符串类型,明明是字符串类型,为什么content-type莫名其妙变成了application/json,搜索MessageConverter子类下的所有application/json,即MessageProperties类下的成员变量CONTENT_TYPE_JSON
在Jackson2JsonMessageConverter和JsonMessageConverter中设置了content-type为application/json
由此推断,是amqpTemplate的消息转换器被更改了,全局搜索Jackson2JsonMessageConverter和JsonMessageConverter,发下了如下代码:
果然amqpTemplate的MessageConverter被更改,查看spring配置
MessageQueueServiceImpl、AgoraPushServiceImpl中使用的RabbitTemplate都是这里配置的同一个对象,springIOC注入的对象默认是单例的,故在其他地方调用sendMsgToAgora后,会将amqpTemplate的MessageConverter改成Jackson2JsonMessageConverter
4.2 消费者代码追踪
查看MessageConsumer代码,在MessageConsumer类上注解了@RabbitListener,在handleMessage上注解了@RabbitHandler,故当队列有消息时,会通过handleMessage方法来消费。那为什么会出现org.springframework.amqp.AmqpException: No method found for class [B这个异常呢?
查询相关博客:https://blog.csdn.net/u013905744/article/details/86736536
为什么一个普通的方法加上@RabbitListener注解就能接收消息了呢? 先总结来说,有一个BeanPostProcessor来处理这个注解,把注解相关的内容取出来,封装成一个RabbitListenerEndPoint。然后给每个Endpoint创建一个MessageListenerContainer,在这个container中注册一个MessageListener,在这个MessageListener中创建了一个HandlerAdapter,这个adapter与rabbitmq broker建立一个connection,接收rabbitmq broker push过来的message,放到一个blocking queue中。至此完成消息的接收。 |
根据上述博客描述内容,点开MessageListenerContainer源码:
从上面能看到设置消费者,设置队列等信息,仔细查看doStart方法:
可以看到,@Listener接受消息实现在这里。关键代码this.initializeConsumers();
关键代码this.taskExecutor.execute(new AsyncMessageProcessingConsumer(consumer)); 点击AsyncMessageProcessingConsumer构造方法:
在点开BlockingQueueConsumer类,查看handle方法:
消费者的消息类型设置在红框中,故可以查看consumer接受消息时的content-type类型,查看MessageListenerContainer属性,使用DefaultMessagePropertiesConverter
点开MessageProperties:
查看代码得知,消息的属性设置来源于BasicProperties,而BasicProperties中的消息来源于消息本身的属性。但这样说,消息的接受最终会和消息的发送方适配?
查阅博客:https://www.jianshu.com/p/382d6f609697
@RabbitListener可以标注在类上面,当使用在类上面的时候,需要配合@RabbitHandler注解一起使用,@RabbitListener标注在类上面表示当有收到消息的时候,就交给带有@RabbitHandler的方法处理,具体找哪个方法处理,需要跟进MessageConverter转换后的java对象。 @RabbitListener注解指定目标方法来作为消费消息的方法,通过注解参数指定所监听的队列或者Binding。使用@RabbitListener可以设置一个自己明确默认值的RabbitListenerContainerFactory对象 如果消息属性中没有指定content_type,则接收消息的处理方法接收类型是byte[],如果消息属性中指定content_type为text,则接收消息的处理方法的参数类型是String类型。不管有没有指定content_type,处理消息方法的参数类型是Message都不会报错 |
由此我们得知,当@RabbitListener与@RabbitHandler配合使用时,会将不同的消息适配到合适的方法上,当消息content-type变成application/json时,而我们处理消息的方法只有图下方法,没有适配到其他方法,故会抛出No method found for class [B,导致队列没有消费者,致使mq阻塞。
当@RabbitListener注解到目标方法时,此异常解决。但消息类型缺被转成一串数字?为什么呢?点开MessagingMessageListenerAdapter
如果没有配置MessageConverter,MessagingMessageListenerAdapter使用的MessagingMessageConverter会初始化一个SimpleMessageConverter,点开SimpleMessageConverter,查看fromMessage方法:
如果contentType已text开头,消息会初始化为字符串
content = new String(message.getBody(), encoding);
如果等于application/x-java-serialized-object,消息会反序列化
content = SerializationUtils.deserialize(this.createObjectInputStream(new ByteArrayInputStream(message.getBody()), this.codebaseUrl));
如果上面全部不满足:content = message.getBody();
最终以object方式返回,因此最终接受消息是字节数组。
5. 问题复现 ①停掉开发环境consumer,清空visitLog队列 ②调用/api/test/push接口 ③测试/visitlog/newLog接口 ④查看消息 看到消息content-type变成application/json ⑥本地启动consumer,抛出异常 ⑦将@RabbitListener注解到方法上,重启consumer,出现字节数组
6. 总结与反思
根本原因,queue的生产者与消费者content-type不一致。如果在后面代码中,消息发送方变成发送序列化对象,同样会抛出No method found for class [B,造成消息队列阻塞。
在开发中,应该限定每个queue的content-type,不能发送其他类型数据;同一项目一个队列应该配置唯一对应的RabbitTemplate,而不是RabbitTemplate对应多个队列。