详细聊聊RabbitMQ竟无法反序列化List问题

编辑: admin 分类: java 发布时间: 2021-12-03 来源:互联网
目录
  • 前言
  • 问题重现
    • 项目依赖
    • 发送方
    • 接收方
  • 错误日志
    • 分析问题原因
      • 解决办法
        • 总结

          前言

          最近在接到了一个需求,大概是通过RabbitMq给xx子系统同步用户数据,要提供单个同步和批量同步。内心暗喜这不简单的很嘛。三下五除二就把代码给写完了,大概长这样:

          public void syncUserSingle(User user) {
              // 省略一大堆业务代码
              rabbitTemplate.convertAndSend("q_sync_user_single", user);
          }
          
          public void syncUserBatch(List<User> userList) {
              // 省略一大堆业务代码
              rabbitTemplate.convertAndSend("q_sync_user_batch", userList);
          }
          
          

          但是在联调的过程中,遇到了一个比较奇葩的问题。单个用户进行同步时,子系统可以正常消费。然后进行批量同步的时候,子系统报错了。并抛出java.lang.ClassCastException提示 LinkedHashMap cannot xxxx class 。于是负责子系统的哥们笑嘻嘻的(表面笑嘻嘻)走过来对我说,不是约定List 为啥发个Map过来?

          看到这个错误,着实让我摸不到头脑。顿时一堆疑问涌上心头, 为啥单个对象可以,List就不行呢?我发的是List 数据,为啥变成Map了?虽然一大堆疑问,但是只能笑嘻嘻的说,我检查一下哈。

          问题重现

          项目依赖

          <?xml version="1.0" encoding="UTF-8"?>
          <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
              <modelVersion>4.0.0</modelVersion>
              <parent>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-parent</artifactId>
                  <version>2.3.2.RELEASE</version>
                  <relativePath/>
              </parent>
              <!-- 省略部分信息 -->
              <dependencies>
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-web</artifactId>
                  </dependency>
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-amqp</artifactId>
                  </dependency>
              </dependencies>
          </project>
          

          发送方

          初始化队列

          @Configuration
          public class QueueConfig {
              @Bean
              public Queue test() {
                  return new Queue("test");
              }
          }
          

          配置RabbitTemplete

          @Configuration
          public class RabbitTemplateConfig {
              @Autowired
              public RabbitTemplateConfig(RabbitTemplate rabbitTemplate) {
                  // 设置Json消息转换器
                  rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
              }
          }
          

          发送接口

          @Controller
          @RequestMapping("/test")
          public class TestController {
          
              @Resource
              private RabbitTemplate template;
          
              @GetMapping("/send")
              public void send() {
                  template.convertAndSend("test", Collections.singletonList(new User(20, "不一样的科技宅")));
              }
          }
          
          

          User类

          @Data
          @AllArgsConstructor
          public class User {
              /**
               * 年龄
               */
              private Integer age;
          
              /**
               * 姓名
               */
              private String name;
          }
          
          

          接收方

          监听配置

          @Configuration
          public class RabbitListenerConfig {
          
              @Bean
              public SimpleRabbitListenerContainerFactory customFactory(SimpleRabbitListenerContainerFactoryConfigurer configurer,
                                                                        ConnectionFactory connectionFactory) {
                  SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
                  // 设置消息转换器
                  factory.setMessageConverter(new Jackson2JsonMessageConverter());
                  configurer.configure(factory, connectionFactory);
                  return factory;
              }
          
          }
          
          

          接收方

          @Service
          public class UserService {
          
              public void save(List<User> userList) {
                  userList.forEach(System.out::println);
              }
              
          }
          @Componentpublic class Receiver {    @Resource    private UserService userService;    @RabbitListener(queues = "test", containerFactory = "customFactory")    public void receive(@Payload List<User> msg) {        userService.save(msg);    }}
          
          

          错误日志

          ❝好家伙果然失败了,这百分百必现的bug呀。❞

          分析问题原因

          首先错误信息是在消费端抛出来的,按理应该是消费端出问题概率较大。但是如果和他说的一样,我生产端发送的消息就是错误的,从而导致消费端出问题呢?这对这个疑问,我先断开消费端,然后发送一条消息,并通过Rabbitmq的管控台来查看消息的内容是否正确。

          消息内容如下图所示:

          通过上图可以发现,消息体(payload)是一个标准的json串,并且TypeId也是List,并不是错误信息中的LinkedHashMap。哈哈哈,到此可以石锤是消费端反序列化的问题了。赶紧把锅甩出去,抽他呀的(自嗨而已),我写的代码怎么可能有bug。

          对我爱学习的我,肯定不愿意就这样算了。必须刨根问底,给他上一课。于是我在google一圈发现这竟然是这个bug。有个老哥也发现了,并提交了一个issues: spring-ampq/issues/1279。

          大致是说:尝试从 Spring Boot 2.3.1 升级到 2.3.3,然后再升级到 2.3.6。错误信息依然是:List<Foo> foos是LikedHashMap,而不是Foo对象。并通过远程调试确认了这种情况。出于某种原因,他认为没有正确使用泛型类型。恢复到 Spring-AMQP 2.2.7 使它再次工作,并且对象确实是Foo。

          然后garyrussell这个人说:他们添加了对抽象类反序列化的支持,如果配置不正确,这会对消息转换器产生一些副作用。然后调查了一下,确认这是一个错误。是由于List是抽象的,新代码认为它不能反序列化。

          解决方法是:

          converter.setAlwaysConvertToInferredType(true);
          

          后面还提到在 GH-1729: Fix JSON Regression修复这个问题,修复的代码如下:

          通过阅读代码发现,修改前的逻辑是: 如果推断类型是抽象的,则返回false也就代表不能转换成推断类型。然后被转换成LinkedHashMap。这也就是出现 LinkedHashMap cannot cast xxxx class的主要原因。

          修改后变成了:如果推断类型是抽象的并且不是容器类型,返回false。也就意味着,虽然推断类型是抽象的,但是如果是容器类型,并且容器内的对象不是抽象的,则可以被转换。这样一来避免了上述问题的产生了。

          前面还提到了通过增加配置来解决。解决起来就相对简单粗暴了,始终转换推断类型。

          解决办法

          到此问题分析完毕,简单总结一下解决方法。主要有两种:

          1、在消费端开启如下配置即可:

          // 始终转换推断类型
          converter.setAlwaysConvertToInferredType(true);
          

          2、升级版本:由于GH-1729: Fix JSON Regression合并到了2.2.13.RELEASE。所以只需要将 spring-amqp 升级到 2.2.13.RELEASE 或以上。或者升级SpringBoot版本到2.3.7.RELEASE。

          总结

          到此这篇关于RabbitMQ竟无法反序列化List的文章就介绍到这了,更多相关RabbitMQ无法反序列化List内容请搜索自由互联以前的文章或继续浏览下面的相关文章希望大家以后多多支持自由互联!

          【文章出处:http://www.yidunidc.com/hkzq.html欢迎转载】